API: Allow to answer file upload questions during ticket redemption

This commit is contained in:
Raphael Michel
2021-01-07 10:16:47 +01:00
parent 5b81507600
commit 01c3b08583
4 changed files with 82 additions and 3 deletions

View File

@@ -22,7 +22,7 @@ from pretix.api.views import RichOrderingFilter
from pretix.api.views.order import OrderPositionFilter from pretix.api.views.order import OrderPositionFilter
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
Checkin, CheckinList, Event, Order, OrderPosition, CachedFile, Checkin, CheckinList, Event, Order, OrderPosition, Question,
) )
from pretix.base.services.checkin import ( from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, perform_checkin, CheckInError, RequiredQuestionsError, perform_checkin,
@@ -302,7 +302,10 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
for q in op.item.questions.filter(ask_during_checkin=True): for q in op.item.questions.filter(ask_during_checkin=True):
if str(q.pk) in aws: if str(q.pk) in aws:
try: try:
given_answers[q] = q.clean_answer(aws[str(q.pk)]) if q.type == Question.TYPE_FILE:
given_answers[q] = self._handle_file_upload(aws[str(q.pk)])
else:
given_answers[q] = q.clean_answer(aws[str(q.pk)])
except ValidationError: except ValidationError:
pass pass
@@ -352,3 +355,25 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
'require_attention': op.item.checkin_attention or op.order.checkin_attention, 'require_attention': op.item.checkin_attention or op.order.checkin_attention,
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data 'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
}, status=201) }, status=201)
def _handle_file_upload(self, data):
try:
cf = CachedFile.objects.get(
session_key=f'api-upload-{str(type(self.request.user or self.request.auth))}-{(self.request.user or self.request.auth).pk}',
file__isnull=False,
pk=data[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))
return cf.file

View File

@@ -1026,7 +1026,7 @@ class Question(LoggedModel):
(TYPE_PHONENUMBER, _("Phone number")), (TYPE_PHONENUMBER, _("Phone number")),
) )
UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME] UNLOCALIZED_TYPES = [TYPE_DATE, TYPE_TIME, TYPE_DATETIME]
ASK_DURING_CHECKIN_UNSUPPORTED = [TYPE_FILE, TYPE_PHONENUMBER] ASK_DURING_CHECKIN_UNSUPPORTED = [TYPE_PHONENUMBER]
event = models.ForeignKey( event = models.ForeignKey(
Event, Event,
@@ -1069,6 +1069,7 @@ class Question(LoggedModel):
) )
ask_during_checkin = models.BooleanField( ask_during_checkin = models.BooleanField(
verbose_name=_('Ask during check-in instead of in the ticket buying process'), verbose_name=_('Ask during check-in instead of in the ticket buying process'),
help_text=_('Not supported by all check-in apps for all question types.'),
default=False default=False
) )
hidden = models.BooleanField( hidden = models.BooleanField(

View File

@@ -1,6 +1,7 @@
from datetime import timedelta from datetime import timedelta
import dateutil import dateutil
from django.core.files import File
from django.db import transaction from django.db import transaction
from django.db.models.functions import TruncDate from django.db.models.functions import TruncDate
from django.dispatch import receiver from django.dispatch import receiver
@@ -125,6 +126,14 @@ def _save_answers(op, answers, given_answers):
else: else:
qa = op.answers.create(question=q, answer=", ".join([str(o) for o in a])) qa = op.answers.create(question=q, answer=", ".join([str(o) for o in a]))
qa.options.add(*a) qa.options.add(*a)
elif isinstance(a, File):
if q in answers:
qa = answers[q]
else:
qa = op.answers.create(question=q, answer=str(a))
qa.file.save(a.name, a, save=False)
qa.answer = 'file://' + qa.file.name
qa.save()
else: else:
if q in answers: if q in answers:
qa = answers[q] qa = answers[q]

View File

@@ -7,6 +7,7 @@ from unittest import mock
from urllib.parse import quote as urlquote from urllib.parse import quote as urlquote
import pytest import pytest
from django.core.files.base import ContentFile
from django.utils.timezone import now from django.utils.timezone import now
from django_countries.fields import Country from django_countries.fields import Country
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
@@ -995,3 +996,46 @@ def test_question_multiple_choice(token_client, organizer, clist, event, order,
with scopes_disabled(): with scopes_disabled():
assert order.positions.first().answers.get(question=question[0]).answer == 'M, L' assert order.positions.first().answers.get(question=question[0]).answer == 'M, L'
assert set(order.positions.first().answers.get(question=question[0]).options.all()) == {question[1], question[2]} assert set(order.positions.first().answers.get(question=question[0]).options.all()) == {question[1], question[2]}
@pytest.mark.django_db
def test_question_upload(token_client, organizer, clist, event, order, 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']
with scopes_disabled():
p = order.positions.first()
question[0].type = 'F'
question[0].save()
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
organizer.slug, event.slug, clist.pk, p.pk
), {}, format='json')
assert resp.status_code == 400
assert resp.data['status'] == 'incomplete'
with scopes_disabled():
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
organizer.slug, event.slug, clist.pk, p.pk
), {'answers': {question[0].pk: "invalid"}}, format='json')
assert resp.status_code == 400
assert resp.data['status'] == 'incomplete'
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
organizer.slug, event.slug, clist.pk, p.pk
), {'answers': {question[0].pk: file_id_png}}, format='json')
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
with scopes_disabled():
assert order.positions.first().answers.get(question=question[0]).answer.startswith('file://')
assert order.positions.first().answers.get(question=question[0]).file