Compare commits

..
Author SHA1 Message Date
Raphael Michel f346acb0d2 API: Only require transmission fields depending on method logic (#6765)
* API: Do not mark all invoice transmission fields as required

* Fix test
2026-11-13 14:34:41 +01:00
Phin Wolkwitz 0478559fd7 Accounting report: Add annotation and full daterange… (Z#23245563) (#6742)
Add an annotation and full daterange in accounting reports if "all time" is selected
2026-11-13 12:33:22 +01:00
Raphael Michel 137059dd4e Customer account login: Add rate limit (Z#23252382) (#6760)
* Customer account login: Add rate limit

* Apply suggestion from @raphaelm
2026-11-11 10:29:51 +01:00
Raphael Michel bd0b55bdd3 Reusable media API: Limit data exposed on cross-organizer requests (Z#23253153) (#6761) 2026-11-11 10:29:02 +01:00
16 changed files with 246 additions and 148 deletions

No files matched your search

+2
View File
@@ -197,6 +197,8 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
'linked_giftcard',
'info',
'notes',
# When adding fields, check if they should be hidden in ReusableMediaViewSet
# for cross-organizer lookups
)
+10 -9
View File
@@ -117,6 +117,7 @@ class TransmissionInfoSerializer(serializers.Serializer):
super().__init__(*args, **kwargs)
for k, v in transmission_type.invoice_address_form_fields.items():
self.fields[k] = form_field_to_serializer_field(v)
self.fields[k].required = False # invoice_address_form_fields_required is checked by parent serializer
class InvoiceAddressSerializer(I18nAwareModelSerializer):
@@ -1282,7 +1283,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
'Orders cannot have more than %(max)s positions.' % {'max': settings.PRETIX_MAX_ORDER_SIZE}
)
errs = defaultdict(dict)
errs = [{} for p in data]
if any([p.get('positionid') for p in data]):
if not all([p.get('positionid') for p in data]):
for i, p in enumerate(data):
@@ -1311,16 +1312,16 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
last_posid = p['positionid']
elif any([p.get('addon_to') for p in data]):
errs = {
i: {'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]}
for i, p in enumerate(data)
}
errs = [
{'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]}
for p in data
]
else:
for i, p in enumerate(data):
p['positionid'] = i + 1
if errs:
raise ValidationError(dict(errs))
if any(errs):
raise ValidationError(errs)
return data
def create(self, validated_data):
@@ -1437,7 +1438,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
# use further down
del quota_diff_for_locking, voucher_diff_for_locking, seat_diff_for_locking
errs = defaultdict(dict)
errs = [{} for p in positions_data]
for i, pos_data in enumerate(positions_data):
if pos_data.get('voucher'):
@@ -1566,7 +1567,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
)
]
if errs:
if any(errs):
raise ValidationError({'positions': errs})
if validated_data.get('locale', None) is None:
+9 -7
View File
@@ -19,7 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from collections import defaultdict
from decimal import Decimal
from rest_framework import serializers
@@ -39,20 +38,23 @@ class VoucherListSerializer(serializers.ListSerializer):
codes = set()
seats = set()
errs = defaultdict(dict)
for i, voucher_data in enumerate(validated_data):
errs = []
err = False
for voucher_data in validated_data:
if voucher_data.get('seat') and (voucher_data.get('seat'), voucher_data.get('subevent')) in seats:
errs[i] = {'code': ['Duplicate seat ID in request.']}
err = True
errs.append({'code': ['Duplicate seat ID in request.']})
continue
else:
seats.add((voucher_data.get('seat'), voucher_data.get('subevent')))
if voucher_data['code'] in codes:
errs[i] = {'code': ['Duplicate voucher code in request.']}
err = True
errs.append({'code': ['Duplicate voucher code in request.']})
else:
codes.add(voucher_data['code'])
if errs:
errs.append({})
if err:
raise ValidationError(errs)
return super().create(validated_data)
+11 -2
View File
@@ -183,9 +183,18 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
type=s.validated_data["type"],
identifier=s.validated_data["identifier"],
)
m.customer = None # not relevant for cross-organizer
s = self.get_serializer(m)
return Response({"result": s.data})
data = s.data
# Remove fields that are not relevant for cross-organizer usage
data["customer"] = None
data["linked_orderposition"] = None
data["linked_orderpositions"] = []
data["info"] = {}
data["notes"] = None
data["claim_token"] = None
data["label"] = None
return Response({"result": data})
except ReusableMedium.DoesNotExist:
mt = MEDIA_TYPES.get(s.validated_data["type"])
if mt:
+28 -17
View File
@@ -27,6 +27,7 @@ from decimal import Decimal
from django import forms
from django.db.models import F, Sum
from django.db.models.aggregates import Min
from django.db.models.functions import Coalesce
from django.utils.formats import date_format
from django.utils.html import escape
@@ -41,7 +42,8 @@ from reportlab.platypus import (
from pretix.base.exporter import BaseExporter
from pretix.base.models import (
GiftCardTransaction, OrderFee, OrderPayment, OrderRefund, Transaction,
GiftCardTransaction, Order, OrderFee, OrderPayment, OrderRefund,
Transaction,
)
from pretix.base.templatetags.money import money_filter, tax_rate_format
from pretix.base.timeframes import (
@@ -116,25 +118,34 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
dt_start, df_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(
now(), form_data["date_range"], self.timezone
)
if dt_start:
filters.append(
_("Begin")
+ ": "
+ date_format(
else:
dt_start = Order.objects.filter(
event__in=self.events,
).aggregate(min_datetime=Min('datetime'))['min_datetime']
df_end = now()
if dt_start:
filters.append(
"{label}: {value}{hint}".format(
label=_("Begin"),
value=date_format(
dt_start.astimezone(self.timezone), "SHORT_DATETIME_FORMAT"
)
),
hint="" if form_data["date_range"] else " ({})".format(_("first order received"))
)
if df_end:
filters.append(
_("End")
+ ": "
+ date_format(
(df_end - datetime.timedelta.resolution).astimezone(
self.timezone
),
"SHORT_DATETIME_FORMAT",
)
)
if df_end:
filters.append(
_("End")
+ ": "
+ date_format(
(df_end - datetime.timedelta.resolution).astimezone(
self.timezone
),
"SHORT_DATETIME_FORMAT",
)
)
if not form_data["no_testmode"]:
filters.append(_("Report includes test orders which may be deleted later!"))
+7
View File
@@ -75,6 +75,7 @@ class AuthenticationForm(forms.Form):
'unverified': _("You have not yet activated your account and set a password. Please click the link in the "
"email we sent you. In case you cannot find it, click \"Forgot your password?\" to receive "
"a new email."),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
}
def __init__(self, request=None, *args, **kwargs):
@@ -93,6 +94,12 @@ class AuthenticationForm(forms.Form):
email = self.cleaned_data.get('email')
password = self.cleaned_data.get('password')
if rate_limit("customer_login", include_ip_from_request=self.request, max_num=10, expire_time=300):
raise forms.ValidationError(
self.error_messages['rate_limit'],
code='rate_limit',
)
if email is not None and password:
try:
u = self.request.organizer.customers.get(email=email.lower(), provider__isnull=True)
+1
View File
@@ -507,6 +507,7 @@ REST_FRAMEWORK = {
],
'EXCEPTION_HANDLER': 'pretix.api.exception.custom_exception_handler',
'UNICODE_JSON': False,
'LIST_SERIALIZER_ERRORS_AS_DICT': False
}
+1
View File
@@ -57,6 +57,7 @@ filterwarnings =
ignore:teardown:pytest.PytestWarning
ignore:avoid running initialization queries:RuntimeWarning
ignore:.*The 'strip_cdata' option of HTMLParser.*:DeprecationWarning:
ignore:The list-based error format for `ListSerializer`:rest_framework.deprecation.RemovedInDRF320Warning
[coverage:run]
+13 -16
View File
@@ -424,7 +424,7 @@ def test_cartpos_create_answer_validation(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'answers': {0: {'question': ['The specified question does not belong to this event.']}}}
assert resp.data == {'answers': [{'question': ['The specified question does not belong to this event.']}]}
res['answers'][0]['question'] = question.pk
res['answers'][0]['options'] = [question.options.first().pk]
@@ -434,9 +434,8 @@ def test_cartpos_create_answer_validation(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'answers': {
0: {'non_field_errors': ['You should not specify options if the question is not of a choice type.']}
}}
assert resp.data == {
'answers': [{'non_field_errors': ['You should not specify options if the question is not of a choice type.']}]}
question.type = Question.TYPE_CHOICE
question.save()
@@ -448,7 +447,7 @@ def test_cartpos_create_answer_validation(token_client, organizer, event, item,
)
assert resp.status_code == 400
assert resp.data == {
'answers': {0: {'non_field_errors': ['You need to specify options if the question is of a choice type.']}}}
'answers': [{'non_field_errors': ['You need to specify options if the question is of a choice type.']}]}
with scopes_disabled():
question.options.create(answer="L")
@@ -462,8 +461,7 @@ def test_cartpos_create_answer_validation(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {
'answers': {0: {'non_field_errors': ['You can specify at most one option for this question.']}}}
assert resp.data == {'answers': [{'non_field_errors': ['You can specify at most one option for this question.']}]}
r = token_client.post(
'/api/v1/upload',
@@ -535,7 +533,7 @@ def test_cartpos_create_answer_validation(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'answers': {0: {'non_field_errors': ['A valid number is required.']}}}
assert resp.data == {'answers': [{'non_field_errors': ['A valid number is required.']}]}
question.type = Question.TYPE_BOOLEAN
question.save()
@@ -575,8 +573,7 @@ def test_cartpos_create_answer_validation(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {
'answers': {0: {'non_field_errors': ['Please specify "true" or "false" for boolean questions.']}}}
assert resp.data == {'answers': [{'non_field_errors': ['Please specify "true" or "false" for boolean questions.']}]}
question.type = Question.TYPE_DATE
question.save()
@@ -602,7 +599,7 @@ def test_cartpos_create_answer_validation(token_client, organizer, event, item,
)
assert resp.status_code == 400
assert resp.data == {
'answers': {0: {'non_field_errors': ['Date has wrong format. Use one of these formats instead: YYYY-MM-DD.']}}}
'answers': [{'non_field_errors': ['Date has wrong format. Use one of these formats instead: YYYY-MM-DD.']}]}
question.type = Question.TYPE_DATETIME
question.save()
@@ -627,9 +624,9 @@ def test_cartpos_create_answer_validation(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'answers': {0: {'non_field_errors': [
assert resp.data == {'answers': [{'non_field_errors': [
'Datetime has wrong format. Use one of these formats instead: '
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].']}}}
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].']}]}
question.type = Question.TYPE_TIME
question.save()
@@ -654,8 +651,8 @@ def test_cartpos_create_answer_validation(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'answers': {0: {'non_field_errors': [
'Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].']}}}
assert resp.data == {'answers': [
{'non_field_errors': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].']}]}
@pytest.mark.django_db
@@ -1390,7 +1387,7 @@ def test_cartpos_create_bulk_validation_error_in_addon(token_client, organizer,
'data': None,
'success': False,
'errors': {
'addons': {0: {'item': ['Invalid pk "-1" - object does not exist.']}}
'addons': [{'item': ['Invalid pk "-1" - object does not exist.']}]
}
}
]
+3 -3
View File
@@ -958,9 +958,9 @@ def test_item_create_with_addon(token_client, organizer, event, item, category,
format='json'
)
assert resp.status_code == 400
assert resp.data in [
{"addons": {0: "The minimum count needs to be equal to or greater than zero."}},
{"addons": {0: {"min_count": ["Ensure this value is greater than or equal to 0."]}}},
assert resp.content.decode() in [
'{"addons":["The minimum count needs to be equal to or greater than zero."]}',
'{"addons":[{"min_count":["Ensure this value is greater than or equal to 0."]}]}',
]
with scopes_disabled():
assert 2 == Item.objects.all().count()
-1
View File
@@ -333,7 +333,6 @@ def test_order_update_transmission_validation(token_client, organizer, event, or
assert resp.status_code == 400
assert resp.data == {"invoice_address": {"transmission_info": {
"transmission_it_sdi_pec": ["Enter a valid email address.", "Enter a valid email address."],
"transmission_it_sdi_recipient_code": ["This field is required."]
}}}
resp = token_client.patch(
+140 -82
View File
@@ -978,7 +978,7 @@ def test_order_create_position_secret_optional(token_client, organizer, event, i
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'secret': ['You cannot assign a position secret that already exists.']}}}
assert resp.data == {'positions': [{'secret': ['You cannot assign a position secret that already exists.']}]}
@pytest.mark.django_db
@@ -1024,7 +1024,7 @@ def test_order_create_fee_type_validation(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'fees': {0: {'fee_type': ['"unknown" is not a valid choice.']}}}
assert resp.data == {'fees': [{'fee_type': ['"unknown" is not a valid choice.']}]}
@pytest.mark.django_db
@@ -1143,7 +1143,7 @@ def test_order_create_tax_rule_wrong_event(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'fees': {0: {'tax_rule': ['The specified tax rate does not belong to this event.']}}}
assert resp.data == {'fees': [{'tax_rule': ['The specified tax rate does not belong to this event.']}]}
@pytest.mark.django_db
@@ -1158,7 +1158,7 @@ def test_order_create_subevent_not_allowed(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'subevent': ['You cannot set a subevent for this event.']}}}
assert resp.data == {'positions': [{'subevent': ['You cannot set a subevent for this event.']}]}
@pytest.mark.django_db
@@ -1185,7 +1185,7 @@ def test_order_create_subevent_validation(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'subevent': ['You need to set a subevent.']}}}
assert resp.data == {'positions': [{'subevent': ['You need to set a subevent.']}]}
res['positions'][0]['subevent'] = subevent2.pk
resp = token_client.post(
@@ -1194,7 +1194,7 @@ def test_order_create_subevent_validation(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'subevent': ['The specified subevent does not belong to this event.']}}}
assert resp.data == {'positions': [{'subevent': ['The specified subevent does not belong to this event.']}]}
@pytest.mark.django_db
@@ -1210,7 +1210,7 @@ def test_order_create_item_validation(token_client, organizer, event, item, item
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'item': ['The specified item is not active.']}}}
assert resp.data == {'positions': [{'item': ['The specified item is not active.']}]}
item.active = True
item.save()
@@ -1222,7 +1222,7 @@ def test_order_create_item_validation(token_client, organizer, event, item, item
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'item': ['The specified item does not belong to this event.']}}}
assert resp.data == {'positions': [{'item': ['The specified item does not belong to this event.']}]}
with scopes_disabled():
var2 = item2.variations.create(value="A")
@@ -1236,7 +1236,7 @@ def test_order_create_item_validation(token_client, organizer, event, item, item
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'variation': ['You cannot specify a variation for this item.']}}}
assert resp.data == {'positions': [{'variation': ['You cannot specify a variation for this item.']}]}
with scopes_disabled():
var1 = item.variations.create(value="A")
@@ -1248,7 +1248,7 @@ def test_order_create_item_validation(token_client, organizer, event, item, item
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'item': ['The product "Budget Ticket" is not assigned to a quota.']}}}
assert resp.data == {'positions': [{'item': ['The product "Budget Ticket" is not assigned to a quota.']}]}
with scopes_disabled():
quota.variations.add(var1)
@@ -1267,7 +1267,7 @@ def test_order_create_item_validation(token_client, organizer, event, item, item
)
assert resp.status_code == 400
assert resp.data == {
'positions': {0: {'variation': ['The specified variation does not belong to the specified item.']}}}
'positions': [{'variation': ['The specified variation does not belong to the specified item.']}]}
res['positions'][0]['variation'] = None
resp = token_client.post(
@@ -1276,7 +1276,7 @@ def test_order_create_item_validation(token_client, organizer, event, item, item
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'variation': ['You should specify a variation for this item.']}}}
assert resp.data == {'positions': [{'variation': ['You should specify a variation for this item.']}]}
@pytest.mark.django_db
@@ -1294,7 +1294,7 @@ def test_order_create_subevent_disabled(token_client, organizer, event, item, su
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'item': ['The product "Budget Ticket" is not available on this date.']}}}
assert resp.data == {'positions': [{'item': ['The product "Budget Ticket" is not available on this date.']}]}
s.delete()
resp = token_client.post(
@@ -1326,7 +1326,7 @@ def test_order_create_subevent_variation_disabled(token_client, organizer, event
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'item': ['The product "Budget Ticket" is not available on this date.']}}}
assert resp.data == {'positions': [{'item': ['The product "Budget Ticket" is not available on this date.']}]}
s.delete()
resp = token_client.post(
@@ -1453,15 +1453,16 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
)
assert resp.status_code == 400
assert resp.data == {
'positions': {
1: {
'positions': [
{},
{
'addon_to': [
'If you set addon_to, you need to make sure that the '
'referenced position ID exists and is transmitted directly '
'before its add-ons.'
]
}
}
]
}
res['positions'] = [
@@ -1492,14 +1493,10 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {
0:
{'positionid': [
"If you set addon_to on any position, you need to specify position IDs manually."]},
1:
{'positionid': [
"If you set addon_to on any position, you need to specify position IDs manually."]}
}}
assert resp.data == {'positions': [
{'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]},
{'positionid': ["If you set addon_to on any position, you need to specify position IDs manually."]}
]}
res['positions'] = [
{
@@ -1529,11 +1526,12 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
)
assert resp.status_code == 400
assert resp.data == {
'positions': {
1: {
'positions': [
{},
{
'positionid': ['If you set position IDs manually, you need to do so for all positions.']
}
}
]
}
res['positions'] = [
@@ -1565,11 +1563,12 @@ def test_order_create_positionid_validation(token_client, organizer, event, item
)
assert resp.status_code == 400
assert resp.data == {
'positions': {
1: {
'positions': [
{},
{
'positionid': ['Position IDs need to be consecutive.']
}
}
]
}
res['positions'] = [
@@ -1616,7 +1615,7 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
)
assert resp.status_code == 400
assert resp.data == {
'positions': {0: {'answers': {0: {'question': ['The specified question does not belong to this event.']}}}}}
'positions': [{'answers': [{'question': ['The specified question does not belong to this event.']}]}]}
res['positions'][0]['answers'][0]['question'] = question.pk
res['positions'][0]['answers'][0]['options'] = [question.options.first().pk]
@@ -1626,8 +1625,8 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'answers': {
0: {'non_field_errors': ['You should not specify options if the question is not of a choice type.']}}}}}
assert resp.data == {'positions': [{'answers': [
{'non_field_errors': ['You should not specify options if the question is not of a choice type.']}]}]}
question.type = Question.TYPE_CHOICE
question.save()
@@ -1638,8 +1637,8 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {
'answers': {0: {'non_field_errors': ['You need to specify options if the question is of a choice type.']}}}}}
assert resp.data == {'positions': [
{'answers': [{'non_field_errors': ['You need to specify options if the question is of a choice type.']}]}]}
with scopes_disabled():
question2.options.create(answer="L")
@@ -1654,8 +1653,7 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
)
assert resp.status_code == 400
assert resp.data == {
'positions': {
0: {'answers': {0: {'non_field_errors': ['The specified option does not belong to this question.']}}}}}
'positions': [{'answers': [{'non_field_errors': ['The specified option does not belong to this question.']}]}]}
with scopes_disabled():
question.options.create(answer="L")
@@ -1671,8 +1669,7 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
)
assert resp.status_code == 400
assert resp.data == {
'positions': {
0: {'answers': {0: {'non_field_errors': ['You can specify at most one option for this question.']}}}}}
'positions': [{'answers': [{'non_field_errors': ['You can specify at most one option for this question.']}]}]}
r = token_client.post(
'/api/v1/upload',
@@ -1748,7 +1745,7 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'answers': {0: {'non_field_errors': ['A valid number is required.']}}}}}
assert resp.data == {'positions': [{'answers': [{'non_field_errors': ['A valid number is required.']}]}]}
question.type = Question.TYPE_BOOLEAN
question.save()
@@ -1790,8 +1787,8 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {
0: {'answers': {0: {'non_field_errors': ['Please specify "true" or "false" for boolean questions.']}}}}}
assert resp.data == {
'positions': [{'answers': [{'non_field_errors': ['Please specify "true" or "false" for boolean questions.']}]}]}
question.type = Question.TYPE_DATE
question.save()
@@ -1817,8 +1814,8 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'answers': {
0: {'non_field_errors': ['Date has wrong format. Use one of these formats instead: YYYY-MM-DD.']}}}}}
assert resp.data == {'positions': [{'answers': [
{'non_field_errors': ['Date has wrong format. Use one of these formats instead: YYYY-MM-DD.']}]}]}
question.type = Question.TYPE_DATETIME
question.save()
@@ -1844,9 +1841,9 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'answers': {0: {'non_field_errors': [
assert resp.data == {'positions': [{'answers': [{'non_field_errors': [
'Datetime has wrong format. Use one of these formats instead: '
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].']}}}}}
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].']}]}]}
question.type = Question.TYPE_TIME
question.save()
@@ -1872,8 +1869,8 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'answers': {
0: {'non_field_errors': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].']}}}}}
assert resp.data == {'positions': [{'answers': [
{'non_field_errors': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].']}]}]}
@pytest.mark.django_db
@@ -1913,10 +1910,10 @@ def test_order_create_quota_validation(token_client, organizer, event, item, quo
)
assert resp.status_code == 400
assert resp.data == {
'positions': {
0: {'item': ['There is not enough quota available on quota "Budget Quota" to perform the operation.']},
1: {'item': ['There is not enough quota available on quota "Budget Quota" to perform the operation.']},
}
'positions': [
{'item': ['There is not enough quota available on quota "Budget Quota" to perform the operation.']},
{'item': ['There is not enough quota available on quota "Budget Quota" to perform the operation.']},
]
}
quota.size = 1
@@ -1927,8 +1924,12 @@ def test_order_create_quota_validation(token_client, organizer, event, item, quo
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {
1: {'item': ['There is not enough quota available on quota "Budget Quota" to perform the operation.']}}}
assert resp.data == {
'positions': [
{},
{'item': ['There is not enough quota available on quota "Budget Quota" to perform the operation.']},
]
}
res['force'] = True
resp = token_client.post(
@@ -1960,8 +1961,11 @@ def test_order_create_quota_consume_cart(token_client, organizer, event, item, q
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {
0: {'item': ['There is not enough quota available on quota "Budget Quota" to perform the operation.']}}}
assert resp.data == {
'positions': [
{'item': ['There is not enough quota available on quota "Budget Quota" to perform the operation.']},
]
}
res['consume_carts'] = [cr.cart_id]
resp = token_client.post(
@@ -1996,8 +2000,11 @@ def test_order_create_quota_consume_cart_expired(token_client, organizer, event,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {
0: {'item': ['There is not enough quota available on quota "Budget Quota" to perform the operation.']}}}
assert resp.data == {
'positions': [
{'item': ['There is not enough quota available on quota "Budget Quota" to perform the operation.']},
]
}
@pytest.mark.django_db
@@ -2169,7 +2176,11 @@ def test_order_create_with_blocked_seat(token_client, organizer, event, item, qu
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'seat': ['The selected seat "Seat A1" is not available.']}}}
assert resp.data == {
'positions': [
{'seat': ['The selected seat "Seat A1" is not available.']},
]
}
@pytest.mark.django_db
@@ -2188,7 +2199,11 @@ def test_order_create_with_used_seat(token_client, organizer, event, item, quota
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'seat': ['The selected seat "Seat A1" is not available.']}}}
assert resp.data == {
'positions': [
{'seat': ['The selected seat "Seat A1" is not available.']},
]
}
@pytest.mark.django_db
@@ -2203,7 +2218,11 @@ def test_order_create_with_unknown_seat(token_client, organizer, event, item, qu
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'seat': ['The specified seat does not exist.']}}}
assert resp.data == {
'positions': [
{'seat': ['The specified seat does not exist.']},
]
}
@pytest.mark.django_db
@@ -2217,7 +2236,11 @@ def test_order_create_require_seat(token_client, organizer, event, item, quota,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'seat': ['The specified product requires to choose a seat.']}}}
assert resp.data == {
'positions': [
{'seat': ['The specified product requires to choose a seat.']},
]
}
@pytest.mark.django_db
@@ -2235,7 +2258,11 @@ def test_order_create_unseated(token_client, organizer, event, item, quota, seat
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'seat': ['The specified product does not allow to choose a seat.']}}}
assert resp.data == {
'positions': [
{'seat': ['The specified product does not allow to choose a seat.']},
]
}
@pytest.mark.django_db
@@ -2273,7 +2300,12 @@ def test_order_create_with_duplicate_seat(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {1: {'seat': ['The selected seat "Seat A1" is not available.']}}}
assert resp.data == {
'positions': [
{},
{'seat': ['The selected seat "Seat A1" is not available.']},
]
}
@pytest.mark.django_db
@@ -2778,8 +2810,8 @@ def test_order_create_autopricing_voucher_budget_full(token_client, organizer, e
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {1: {'voucher': ['The voucher has a remaining budget of 0.00, therefore a '
'discount of 1.50 can not be given.']}}}
assert resp.data == {'positions': [{}, {'voucher': ['The voucher has a remaining budget of 0.00, therefore a '
'discount of 1.50 can not be given.']}]}
@pytest.mark.django_db
@@ -2800,8 +2832,8 @@ def test_order_create_voucher_budget_exceeded(token_client, organizer, event, it
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'voucher': ['The voucher has a remaining budget of 3.00, therefore a '
'discount of 4.00 can not be given.']}}}
assert resp.data == {'positions': [{'voucher': ['The voucher has a remaining budget of 3.00, therefore a '
'discount of 4.00 can not be given.']}]}
@pytest.mark.django_db
@@ -2844,7 +2876,11 @@ def test_order_create_voucher_unknown_code(token_client, organizer, event, item,
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'positions': {0: {'voucher': ['Object with code=FOOBAR does not exist.']}}}
assert resp.data == {
'positions': [
{'voucher': ['Object with code=FOOBAR does not exist.']},
]
}
@pytest.mark.django_db
@@ -2863,7 +2899,9 @@ def test_order_create_voucher_redeemed(token_client, organizer, event, item, quo
)
assert resp.status_code == 400
assert resp.data == {
'positions': {0: {'voucher': ['The voucher has already been used the maximum number of times.']}}
'positions': [
{'voucher': ['The voucher has already been used the maximum number of times.']},
]
}
@@ -2886,10 +2924,11 @@ def test_order_create_voucher_redeemed_partially(token_client, organizer, event,
)
assert resp.status_code == 400
assert resp.data == {
'positions': {
1: {'voucher': ['The voucher has already been used the maximum number of times.']},
2: {'voucher': ['The voucher has already been used the maximum number of times.']},
}
'positions': [
{},
{'voucher': ['The voucher has already been used the maximum number of times.']},
{'voucher': ['The voucher has already been used the maximum number of times.']},
]
}
@@ -2910,7 +2949,10 @@ def test_order_create_voucher_item_mismatch(token_client, organizer, event, item
)
assert resp.status_code == 400
assert resp.data == {
'positions': {0: {'voucher': ['This voucher is not valid for this product.']}}}
'positions': [
{'voucher': ['This voucher is not valid for this product.']},
]
}
@pytest.mark.django_db
@@ -2930,7 +2972,10 @@ def test_order_create_voucher_expired(token_client, organizer, event, item, quot
)
assert resp.status_code == 400
assert resp.data == {
'positions': {0: {'voucher': ['This voucher is expired.']}}}
'positions': [
{'voucher': ['This voucher is expired.']},
]
}
@pytest.mark.django_db
@@ -3058,8 +3103,10 @@ def test_create_cart_and_consume_cart_with_addons(token_client, organizer, event
)
assert resp.status_code == 400
assert resp.data == {
'positions': {
1: {'item': ['There is not enough quota available on quota "Addon Quota" to perform the operation.']}}
'positions': [
{},
{'item': ['There is not enough quota available on quota "Addon Quota" to perform the operation.']},
]
}
res['consume_carts'] = ['aaa@api']
@@ -3167,7 +3214,12 @@ def test_order_create_use_medium_other_organizer(token_client, organizer, event,
), format='json', data=res
)
assert resp.data == {
"positions": {0: {"use_reusable_medium": ["The specified medium does not belong to this organizer."]}}}
"positions": [
{
"use_reusable_medium": ["The specified medium does not belong to this organizer."]
}
]
}
assert resp.status_code == 400
@@ -3309,8 +3361,14 @@ def test_order_create_auto_pricing_explicit_discount_not_allowed(token_client, o
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {"positions": {
1: {"discount": ["You can only specify a discount if you do the price computation, but price is not set."]}}}
assert resp.data == {
"positions": [
{},
{
"discount": ["You can only specify a discount if you do the price computation, but price is not set."]
}
]
}
@pytest.mark.django_db
+12 -1
View File
@@ -696,7 +696,7 @@ def test_medium_autocreate_giftcard(token_client, organizer, team):
@pytest.mark.django_db
def test_medium_lookup_cross_organizer(token_client, organizer, organizer2, org2_event, medium2, giftcard2):
def test_medium_lookup_cross_organizer(token_client, organizer, customer, organizer2, org2_event, medium2, giftcard2):
with scopes_disabled():
o = Order.objects.create(
code='FOO', event=org2_event, email='dummy@dummy.test',
@@ -708,7 +708,12 @@ def test_medium_lookup_cross_organizer(token_client, organizer, organizer2, org2
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
medium2.linked_orderpositions.add(op)
medium2.customer = customer
medium2.linked_giftcard = giftcard2
medium2.claim_token = "1234"
medium2.info = {"foo": "bar"}
medium2.notes = "Internal notes"
medium2.label = "Max Muster"
medium2.save()
resp = token_client.post(
@@ -755,3 +760,9 @@ def test_medium_lookup_cross_organizer(token_client, organizer, organizer2, org2
assert resp.data["result"]["organizer"] == "partner"
assert resp.data["result"]["linked_giftcard"] is not None
assert resp.data["result"]["linked_orderposition"] is None
assert resp.data["result"]["linked_orderpositions"] == []
assert resp.data["result"]["customer"] is None
assert resp.data["result"]["info"] == {}
assert resp.data["result"]["notes"] is None
assert resp.data["result"]["claim_token"] is None
assert resp.data["result"]["label"] is None
+3 -4
View File
@@ -409,7 +409,7 @@ def test_subevent_create(team, token_client, organizer, event, subevent, meta_pr
format='json'
)
assert resp.status_code == 400
assert resp.data == {"item_price_overrides": {0: {"item": ["Invalid pk \"555\" - object does not exist."]}}}
assert resp.content.decode() == '{"item_price_overrides":[{"item":["Invalid pk \\"555\\" - object does not exist."]}]}'
@pytest.mark.django_db
@@ -580,7 +580,7 @@ def test_subevent_update(token_client, organizer, event, subevent, item, item2,
format='json'
)
assert resp.status_code == 400
assert resp.data == {"item_price_overrides": {0: {"item": ["Invalid pk \"123\" - object does not exist."]}}}
assert resp.content.decode() == '{"item_price_overrides":[{"item":["Invalid pk \\"123\\" - object does not exist."]}]}'
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
@@ -686,8 +686,7 @@ def test_subevent_update(token_client, organizer, event, subevent, item, item2,
format='json'
)
assert resp.status_code == 400
assert resp.data == {
"variation_price_overrides": {0: {"variation": ["Invalid pk \"123\" - object does not exist."]}}}
assert resp.content.decode() == '{"variation_price_overrides":[{"variation":["Invalid pk \\"123\\" - object does not exist."]}]}'
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
+3 -3
View File
@@ -1061,7 +1061,7 @@ def test_create_multiple_vouchers_one_invalid(token_client, organizer, event, it
], format='json'
)
assert resp.status_code == 400
assert resp.data == {1: {'code': ['Ensure this field has at least 5 characters.']}}
assert resp.data == [{}, {'code': ['Ensure this field has at least 5 characters.']}]
with scopes_disabled():
assert Voucher.objects.count() == 0
@@ -1106,7 +1106,7 @@ def test_create_multiple_vouchers_duplicate_code(token_client, organizer, event,
], format='json'
)
assert resp.status_code == 400
assert resp.data == {1: {'code': ['Duplicate voucher code in request.']}}
assert resp.data == [{}, {'code': ['Duplicate voucher code in request.']}]
with scopes_disabled():
assert Voucher.objects.count() == 0
@@ -1214,7 +1214,7 @@ def test_create_multiple_vouchers_duplicate_seat(token_client, organizer, event,
], format='json'
)
assert resp.status_code == 400
assert resp.data == {1: {'code': ['Duplicate seat ID in request.']}}
assert resp.data == [{}, {'code': ['Duplicate seat ID in request.']}]
with scopes_disabled():
assert Voucher.objects.count() == 0
@@ -311,7 +311,7 @@ def test_renderer_batch_invalid(env, token_client, position):
format='json',
)
assert resp.status_code == 400
assert resp.data == {"parts": {0: {"orderposition": ["Invalid pk \"-2\" - object does not exist."]}}}
assert resp.data == {"parts": [{"orderposition": ["Invalid pk \"-2\" - object does not exist."]}]}
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
@@ -326,7 +326,7 @@ def test_renderer_batch_invalid(env, token_client, position):
format='json',
)
assert resp.status_code == 400
assert resp.data == {"parts": {0: {"override_layout": ["Invalid pk \"-2\" - object does not exist."]}}}
assert resp.data == {"parts": [{"override_layout": ["Invalid pk \"-2\" - object does not exist."]}]}
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),
@@ -341,7 +341,7 @@ def test_renderer_batch_invalid(env, token_client, position):
format='json',
)
assert resp.status_code == 400
assert resp.data == {"parts": {0: {"override_channel": ["Object with identifier=magic does not exist."]}}}
assert resp.data == {"parts": [{"override_channel": ["Object with identifier=magic does not exist."]}]}
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/ticketpdfrenderer/render_batch/'.format(env[0].slug, env[0].slug),