API: Allow to modify order position information (#1904)

This commit is contained in:
Raphael Michel
2021-01-13 14:18:58 +01:00
committed by GitHub
parent 70bf422537
commit d391312aab
8 changed files with 635 additions and 82 deletions

View File

@@ -1,5 +1,6 @@
from datetime import timedelta
from django.core.files import File
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import gettext_lazy
@@ -100,8 +101,15 @@ class CartPositionCreateSerializer(I18nAwareModelSerializer):
for answ_data in answers_data:
options = answ_data.pop('options')
answ = cp.answers.create(**answ_data)
answ.options.add(*options)
if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer')
answ = cp.answers.create(**answ_data, answer='')
answ.file.save(an.name, an, save=False)
answ.answer = 'file://' + answ.file.name
answ.save()
else:
answ = cp.answers.create(**answ_data)
answ.options.add(*options)
return cp
def validate_cart_id(self, cid):

View File

@@ -3,6 +3,7 @@ from collections import Counter, defaultdict
from decimal import Decimal
import pycountry
from django.core.files import File
from django.db.models import F, Q
from django.utils.timezone import now
from django.utils.translation import gettext_lazy
@@ -17,8 +18,9 @@ from pretix.base.channels import get_all_sales_channels
from pretix.base.decimal import round_decimal
from pretix.base.i18n import language
from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Item, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, Seat, SubEvent, TaxRule, Voucher,
CachedFile, Checkin, Invoice, InvoiceAddress, InvoiceLine, Item,
ItemVariation, Order, OrderPosition, Question, QuestionAnswer, Seat,
SubEvent, TaxRule, Voucher,
)
from pretix.base.models.orders import (
CartPosition, OrderFee, OrderPayment, OrderRefund, RevokedTicketSecret,
@@ -94,12 +96,9 @@ class AnswerQuestionIdentifierField(serializers.Field):
class AnswerQuestionOptionsIdentifierField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return [o.identifier for o in instance.options.all()]
class AnswerQuestionOptionsField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return [o.pk for o in instance.options.all()]
if isinstance(instance, WrappedModel) or instance.pk:
return [o.identifier for o in instance.options.all()]
return []
class InlineSeatSerializer(I18nAwareModelSerializer):
@@ -112,12 +111,91 @@ class InlineSeatSerializer(I18nAwareModelSerializer):
class AnswerSerializer(I18nAwareModelSerializer):
question_identifier = AnswerQuestionIdentifierField(source='*', read_only=True)
option_identifiers = AnswerQuestionOptionsIdentifierField(source='*', read_only=True)
options = AnswerQuestionOptionsField(source='*', read_only=True)
class Meta:
model = QuestionAnswer
fields = ('question', 'answer', 'question_identifier', 'options', 'option_identifiers')
def validate_question(self, q):
if q.event != self.context['event']:
raise ValidationError(
'The specified question does not belong to this event.'
)
return q
def _handle_file_upload(self, data):
try:
ao = self.context["request"].user or self.context["request"].auth
cf = CachedFile.objects.get(
session_key=f'api-upload-{str(type(ao))}-{ao.pk}',
file__isnull=False,
pk=data['answer'][len("file:"):],
)
except (ValidationError, IndexError): # invalid uuid
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
except CachedFile.DoesNotExist:
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
allowed_types = (
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
)
if cf.type not in allowed_types:
raise ValidationError('The submitted file "{fid}" has a file type that is not allowed in this field.'.format(fid=data))
if cf.file.size > 10 * 1024 * 1024:
raise ValidationError('The submitted file "{fid}" is too large to be used in this field.'.format(fid=data))
data['options'] = []
data['answer'] = cf.file
return data
def validate(self, data):
if data.get('question').type == Question.TYPE_FILE:
return self._handle_file_upload(data)
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
if not data.get('options'):
raise ValidationError(
'You need to specify options if the question is of a choice type.'
)
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
raise ValidationError(
'You can specify at most one option for this question.'
)
for o in data.get('options'):
if o.question_id != data.get('question').pk:
raise ValidationError(
'The specified option does not belong to this question.'
)
data['answer'] = ", ".join([str(o) for o in data.get('options')])
else:
if data.get('options'):
raise ValidationError(
'You should not specify options if the question is not of a choice type.'
)
if data.get('question').type == Question.TYPE_BOOLEAN:
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
data['answer'] = 'True'
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
data['answer'] = 'False'
else:
raise ValidationError(
'Please specify "true" or "false" for boolean questions.'
)
elif data.get('question').type == Question.TYPE_NUMBER:
serializers.DecimalField(
max_digits=50,
decimal_places=25
).to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATE:
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_TIME:
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATETIME:
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
return data
class CheckinSerializer(I18nAwareModelSerializer):
class Meta:
@@ -205,13 +283,14 @@ class PdfDataSerializer(serializers.Field):
class OrderPositionSerializer(I18nAwareModelSerializer):
checkins = CheckinSerializer(many=True)
checkins = CheckinSerializer(many=True, read_only=True)
answers = AnswerSerializer(many=True)
downloads = PositionDownloadsField(source='*')
downloads = PositionDownloadsField(source='*', read_only=True)
order = serializers.SlugRelatedField(slug_field='code', read_only=True)
pdf_data = PdfDataSerializer(source='*')
pdf_data = PdfDataSerializer(source='*', read_only=True)
seat = InlineSeatSerializer(read_only=True)
country = CompatibleCountryField(source='*')
attendee_name = serializers.CharField(required=False)
class Meta:
model = OrderPosition
@@ -219,12 +298,99 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
'company', 'street', 'zipcode', 'city', 'country', 'state',
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'canceled')
read_only_fields = (
'id', 'order', 'positionid', 'item', 'variation', 'price', 'voucher', 'tax_rate', 'tax_value', 'secret',
'addon_to', 'subevent', 'checkins', 'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data',
'seat', 'canceled'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'request' in self.context and not self.context['request'].query_params.get('pdf_data', 'false') == 'true':
self.fields.pop('pdf_data')
def validate(self, data):
if data.get('attendee_name') and data.get('attendee_name_parts'):
raise ValidationError(
{'attendee_name': ['Do not specify attendee_name if you specified attendee_name_parts.']}
)
if data.get('attendee_name_parts') and '_scheme' not in data.get('attendee_name_parts'):
data['attendee_name_parts']['_scheme'] = self.context['request'].event.settings.name_scheme
if data.get('country'):
if not pycountry.countries.get(alpha_2=data.get('country').code):
raise ValidationError(
{'country': ['Invalid country code.']}
)
if data.get('state'):
cc = str(data.get('country') or self.instance.country or '')
if cc not in COUNTRIES_WITH_STATE_IN_ADDRESS:
raise ValidationError(
{'state': ['States are not supported in country "{}".'.format(cc)]}
)
if not pycountry.subdivisions.get(code=cc + '-' + data.get('state')):
raise ValidationError(
{'state': ['"{}" is not a known subdivision of the country "{}".'.format(data.get('state'), cc)]}
)
return data
def update(self, instance, validated_data):
# Even though all fields that shouldn't be edited are marked as read_only in the serializer
# (hopefully), we'll be extra careful here and be explicit about the model fields we update.
update_fields = [
'attendee_name_parts', 'company', 'street', 'zipcode', 'city', 'country',
'state', 'attendee_email',
]
answers_data = validated_data.pop('answers', None)
name = validated_data.pop('attendee_name', '')
if name and not validated_data.get('attendee_name_parts'):
validated_data['attendee_name_parts'] = {
'_legacy': name
}
for attr, value in validated_data.items():
if attr in update_fields:
setattr(instance, attr, value)
instance.save(update_fields=update_fields)
if answers_data is not None:
qs_seen = set()
answercache = {
a.question_id: a for a in instance.answers.all()
}
for answ_data in answers_data:
options = answ_data.pop('options', [])
if answ_data['question'].pk in qs_seen:
raise ValidationError(f'Question {answ_data["question"]} was sent twice.')
if answ_data['question'].pk in answercache:
a = answercache[answ_data['question'].pk]
if isinstance(answ_data['answer'], File):
a.file.save(answ_data['answer'].name, answ_data['answer'], save=False)
a.answer = 'file://' + a.file.name
else:
for attr, value in answ_data.items():
setattr(a, attr, value)
a.save()
else:
if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer')
a = instance.answers.create(**answ_data, answer='')
a.file.save(an.name, an, save=False)
a.answer = 'file://' + a.file.name
a.save()
else:
a = instance.answers.create(**answ_data)
a.options.set(options)
qs_seen.add(a.question_id)
for qid, a in answercache.items():
if qid not in qs_seen:
a.delete()
return instance
class RequireAttentionField(serializers.Field):
def to_representation(self, instance: OrderPosition):
@@ -425,7 +591,17 @@ class OrderSerializer(I18nAwareModelSerializer):
return instance
class AnswerQuestionOptionsField(serializers.Field):
def to_representation(self, instance: QuestionAnswer):
return [o.pk for o in instance.options.all()]
class SimulatedAnswerSerializer(AnswerSerializer):
options = AnswerQuestionOptionsField(read_only=True, source='*')
class SimulatedOrderPositionSerializer(OrderPositionSerializer):
answers = SimulatedAnswerSerializer(many=True)
addon_to = serializers.SlugRelatedField(read_only=True, slug_field='positionid')
@@ -452,62 +628,8 @@ class PriceCalcSerializer(serializers.Serializer):
del self.fields['subevent']
class AnswerCreateSerializer(I18nAwareModelSerializer):
class Meta:
model = QuestionAnswer
fields = ('question', 'answer', 'options')
def validate_question(self, q):
if q.event != self.context['event']:
raise ValidationError(
'The specified question does not belong to this event.'
)
return q
def validate(self, data):
if data.get('question').type == Question.TYPE_FILE:
raise ValidationError(
'File uploads are currently not supported via the API.'
)
elif data.get('question').type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
if not data.get('options'):
raise ValidationError(
'You need to specify options if the question is of a choice type.'
)
if data.get('question').type == Question.TYPE_CHOICE and len(data.get('options')) > 1:
raise ValidationError(
'You can specify at most one option for this question.'
)
data['answer'] = ", ".join([str(o) for o in data.get('options')])
else:
if data.get('options'):
raise ValidationError(
'You should not specify options if the question is not of a choice type.'
)
if data.get('question').type == Question.TYPE_BOOLEAN:
if data.get('answer') in ['true', 'True', '1', 'TRUE']:
data['answer'] = 'True'
elif data.get('answer') in ['false', 'False', '0', 'FALSE']:
data['answer'] = 'False'
else:
raise ValidationError(
'Please specify "true" or "false" for boolean questions.'
)
elif data.get('question').type == Question.TYPE_NUMBER:
serializers.DecimalField(
max_digits=50,
decimal_places=25
).to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATE:
data['answer'] = serializers.DateField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_TIME:
data['answer'] = serializers.TimeField().to_internal_value(data.get('answer'))
elif data.get('question').type == Question.TYPE_DATETIME:
data['answer'] = serializers.DateTimeField().to_internal_value(data.get('answer'))
return data
class AnswerCreateSerializer(AnswerSerializer):
pass
class OrderFeeCreateSerializer(I18nAwareModelSerializer):
@@ -1044,8 +1166,16 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos.save()
for answ_data in answers_data:
options = answ_data.pop('options', [])
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
if isinstance(answ_data['answer'], File):
an = answ_data.pop('answer')
answ = pos.answers.create(**answ_data, answer='')
answ.file.save(an.name, an, save=False)
answ.answer = 'file://' + answ.file.name
answ.save()
else:
answ = pos.answers.create(**answ_data)
answ.options.add(*options)
pos_map[pos.positionid] = pos
if not simulate:

View File

@@ -763,7 +763,7 @@ with scopes_disabled():
}
class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
class OrderPositionViewSet(mixins.DestroyModelMixin, mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, OrderingFilter)
@@ -783,6 +783,11 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
},
}
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
return ctx
def get_queryset(self):
if self.request.query_params.get('include_canceled_positions', 'false') == 'true':
qs = OrderPosition.all
@@ -951,6 +956,44 @@ class OrderPositionViewSet(mixins.DestroyModelMixin, viewsets.ReadOnlyModelViewS
except Quota.QuotaExceededException as e:
raise ValidationError(str(e))
def update(self, request, *args, **kwargs):
partial = kwargs.get('partial', False)
if not partial:
return Response(
{"detail": "Method \"PUT\" not allowed."},
status=status.HTTP_405_METHOD_NOT_ALLOWED,
)
return super().update(request, *args, **kwargs)
def perform_update(self, serializer):
with transaction.atomic():
old_data = self.get_serializer_class()(instance=serializer.instance, context=self.get_serializer_context()).data
serializer.save()
new_data = serializer.data
if old_data != new_data:
log_data = self.request.data
if 'answers' in log_data:
for a in new_data['answers']:
log_data[f'question_{a["question"]}'] = a["answer"]
log_data.pop('answers', None)
serializer.instance.order.log_action(
'pretix.event.order.modified',
user=self.request.user,
auth=self.request.auth,
data={
'data': [
dict(
position=serializer.instance.pk,
**log_data
)
]
}
)
tickets.invalidate_cache.apply_async(kwargs={'event': serializer.instance.order.event.pk, 'order': serializer.instance.order.pk})
order_modified.send(sender=serializer.instance.order.event, order=serializer.instance.order)
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPaymentSerializer

View File

@@ -1224,6 +1224,9 @@ class AbstractPosition(models.Model):
else self.variation.quotas.filter(subevent=self.subevent))
def save(self, *args, **kwargs):
update_fields = kwargs.get('update_fields', [])
if 'attendee_name_parts' in update_fields:
update_fields.append('attendee_name_cached')
self.attendee_name_cached = self.attendee_name
if self.attendee_name_parts is None:
self.attendee_name_parts = {}

View File

@@ -1634,10 +1634,16 @@ class OrderModifyInformation(OrderQuestionsViewMixin, OrderView):
self.invoice_form.save()
self.order.log_action('pretix.event.order.modified', {
'invoice_data': self.invoice_form.cleaned_data,
'data': [{
k: (f.cleaned_data.get(k).name if isinstance(f.cleaned_data.get(k), File) else f.cleaned_data.get(k))
for k in f.changed_data
} for f in self.forms]
'data': [
dict(
position=f.orderpos.pk,
**{
k: (f.cleaned_data.get(k).name if isinstance(f.cleaned_data.get(k),
File) else f.cleaned_data.get(k))
for k in f.changed_data
}
) for f in self.forms
]
}, user=request.user)
if self.invoice_form.has_changed():
success_message = ('The invoice address has been updated. If you want to generate a new invoice, '

View File

@@ -4,6 +4,7 @@ from decimal import Decimal
from unittest import mock
import pytest
from django.core.files.base import ContentFile
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pytz import UTC
@@ -434,6 +435,19 @@ def test_cartpos_create_answer_validation(token_client, organizer, event, item,
assert resp.status_code == 400
assert resp.data == {'answers': [{'non_field_errors': ['You can specify at most one option for this question.']}]}
r = token_client.post(
'/api/v1/upload',
data={
'media_type': 'image/png',
'file': ContentFile('file.png', 'invalid png content')
},
format='upload',
HTTP_CONTENT_DISPOSITION='attachment; filename="file.png"',
)
assert r.status_code == 201
file_id_png = r.data['id']
res['answers'][0]['options'] = []
res['answers'][0]['answer'] = file_id_png
question.type = Question.TYPE_FILE
question.save()
resp = token_client.post(
@@ -441,8 +455,12 @@ def test_cartpos_create_answer_validation(token_client, organizer, event, item,
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {'answers': [{'non_field_errors': ['File uploads are currently not supported via the API.']}]}
assert resp.status_code == 201
with scopes_disabled():
pos = CartPosition.objects.get(pk=resp.data['id'])
answ = pos.answers.first()
assert answ.file
assert answ.answer.startswith("file://")
question.type = Question.TYPE_CHOICE_MULTIPLE
question.save()

View File

@@ -6,6 +6,7 @@ from unittest import mock
import pytest
from django.core import mail as djmail
from django.core.files.base import ContentFile
from django.utils.timezone import now
from django_countries.fields import Country
from django_scopes import scopes_disabled
@@ -2692,6 +2693,21 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
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")
with scopes_disabled():
res['positions'][0]['answers'][0]['options'] = [
question2.options.first().pk,
]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {
'positions': [{'answers': [{'non_field_errors': ['The specified option does not belong to this question.']}]}]}
with scopes_disabled():
question.options.create(answer="L")
with scopes_disabled():
@@ -2708,6 +2724,19 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
assert resp.data == {
'positions': [{'answers': [{'non_field_errors': ['You can specify at most one option for this question.']}]}]}
r = token_client.post(
'/api/v1/upload',
data={
'media_type': 'image/png',
'file': ContentFile('file.png', 'invalid png content')
},
format='upload',
HTTP_CONTENT_DISPOSITION='attachment; filename="file.png"',
)
assert r.status_code == 201
file_id_png = r.data['id']
res['positions'][0]['answers'][0]['options'] = []
res['positions'][0]['answers'][0]['answer'] = file_id_png
question.type = Question.TYPE_FILE
question.save()
resp = token_client.post(
@@ -2715,9 +2744,13 @@ def test_order_create_answer_validation(token_client, organizer, event, item, qu
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
assert resp.data == {
'positions': [{'answers': [{'non_field_errors': ['File uploads are currently not supported via the API.']}]}]}
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
pos = o.positions.first()
answ = pos.answers.first()
assert answ.file
assert answ.answer.startswith("file://")
question.type = Question.TYPE_CHOICE_MULTIPLE
question.save()
@@ -4609,3 +4642,254 @@ def test_revoked_secret_list(token_client, organizer, event):
))
assert resp.status_code == 200
assert [res] == resp.data['results']
@pytest.mark.django_db
def test_position_update_ignore_fields(token_client, organizer, event, order):
with scopes_disabled():
op = order.positions.first()
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
), format='json', data={
'price': '99.99'
}
)
assert resp.status_code == 200
op.refresh_from_db()
assert op.price == Decimal('23.00')
@pytest.mark.django_db
def test_position_update_only_partial(token_client, organizer, event, order):
with scopes_disabled():
op = order.positions.first()
resp = token_client.put(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
), format='json', data={
'price': '99.99'
}
)
assert resp.status_code == 405
@pytest.mark.django_db
def test_position_update(token_client, organizer, event, order, question):
with scopes_disabled():
op = order.positions.first()
question.type = Question.TYPE_CHOICE_MULTIPLE
question.save()
opt = question.options.create(answer="L")
payload = {
'company': 'VILE',
'attendee_name_parts': {
'full_name': 'Max Mustermann'
},
'street': 'Sesame Street 21',
'zipcode': '99999',
'city': 'Springfield',
'country': 'US',
'state': 'CA',
'attendee_email': 'foo@example.org',
'answers': [
{
'question': question.pk,
'answer': 'ignored',
'options': [opt.pk]
}
]
}
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
), format='json', data=payload
)
assert resp.status_code == 200
assert resp.data['answers'] == [
{
'question': question.pk,
'question_identifier': question.identifier,
'answer': 'L',
'options': [opt.pk],
'option_identifiers': [opt.identifier],
}
]
op.refresh_from_db()
assert op.company == 'VILE'
assert op.attendee_name_cached == 'Max Mustermann'
assert op.attendee_name_parts == {
'_scheme': 'full',
'full_name': 'Max Mustermann'
}
with scopes_disabled():
assert op.answers.get().answer == 'L'
assert op.street == 'Sesame Street 21'
assert op.zipcode == '99999'
assert op.city == 'Springfield'
assert str(op.country) == 'US'
assert op.state == 'CA'
assert op.attendee_email == 'foo@example.org'
le = order.all_logentries().last()
assert le.action_type == 'pretix.event.order.modified'
assert le.parsed_data == {
'data': [
{
'position': op.pk,
'company': 'VILE',
'attendee_name_parts': {
'_scheme': 'full',
'full_name': 'Max Mustermann'
},
'street': 'Sesame Street 21',
'zipcode': '99999',
'city': 'Springfield',
'country': 'US',
'state': 'CA',
'attendee_email': 'foo@example.org',
f'question_{question.pk}': 'L'
}
]
}
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
), format='json', data=payload
)
assert resp.status_code == 200
with scopes_disabled():
assert order.all_logentries().last().pk == le.pk
@pytest.mark.django_db
def test_position_update_legacy_name(token_client, organizer, event, order):
with scopes_disabled():
op = order.positions.first()
payload = {
'attendee_name': 'Max Mustermann',
'attendee_name_parts': {
'_legacy': 'maria'
},
}
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
), format='json', data=payload
)
assert resp.status_code == 400
payload = {
'attendee_name': 'Max Mustermann',
}
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
), format='json', data=payload
)
assert resp.status_code == 200
op.refresh_from_db()
assert op.attendee_name_cached == 'Max Mustermann'
assert op.attendee_name_parts == {
'_legacy': 'Max Mustermann'
}
with scopes_disabled():
assert op.answers.count() == 1 # answer does not get deleted
@pytest.mark.django_db
def test_position_update_state_validation(token_client, organizer, event, order):
with scopes_disabled():
op = order.positions.first()
payload = {
'country': 'DE',
'state': 'BW'
}
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
), format='json', data=payload
)
assert resp.status_code == 400
@pytest.mark.django_db
def test_position_update_question_handling(token_client, organizer, event, order, question):
with scopes_disabled():
op = order.positions.first()
payload = {
'answers': [
{
'question': question.pk,
'answer': 'FOOBAR',
},
{
'question': question.pk,
'answer': 'FOOBAR',
},
]
}
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
), format='json', data=payload
)
assert resp.status_code == 400
payload = {
'answers': [
{
'question': question.pk,
'answer': 'FOOBAR',
},
]
}
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
), format='json', data=payload
)
assert resp.status_code == 200
with scopes_disabled():
assert op.answers.count() == 1
payload = {
'answers': [
]
}
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
), format='json', data=payload
)
assert resp.status_code == 200
with scopes_disabled():
assert op.answers.count() == 0
r = token_client.post(
'/api/v1/upload',
data={
'media_type': 'image/png',
'file': ContentFile('file.png', 'invalid png content')
},
format='upload',
HTTP_CONTENT_DISPOSITION='attachment; filename="file.png"',
)
assert r.status_code == 201
file_id_png = r.data['id']
payload = {
'answers': [
{
"question": question.id,
"answer": file_id_png
}
]
}
question.type = Question.TYPE_FILE
question.save()
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/orderpositions/{}/'.format(
organizer.slug, event.slug, op.pk
), format='json', data=payload
)
assert resp.status_code == 200
with scopes_disabled():
answ = op.answers.get()
assert answ.file
assert answ.answer.startswith("file://")