diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index 20f51e74d5..75919b1cb3 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -22,7 +22,7 @@ from pretix.api.views import RichOrderingFilter from pretix.api.views.order import OrderPositionFilter from pretix.base.i18n import language from pretix.base.models import ( - Checkin, CheckinList, Event, Order, OrderPosition, + CachedFile, Checkin, CheckinList, Event, Order, OrderPosition, Question, ) from pretix.base.services.checkin import ( CheckInError, RequiredQuestionsError, perform_checkin, @@ -302,7 +302,10 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet): for q in op.item.questions.filter(ask_during_checkin=True): if str(q.pk) in aws: 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: pass @@ -352,3 +355,25 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet): 'require_attention': op.item.checkin_attention or op.order.checkin_attention, 'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data }, 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 diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index cab110df86..dddc76bd68 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -1026,7 +1026,7 @@ class Question(LoggedModel): (TYPE_PHONENUMBER, _("Phone number")), ) 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, @@ -1069,6 +1069,7 @@ class Question(LoggedModel): ) ask_during_checkin = models.BooleanField( 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 ) hidden = models.BooleanField( diff --git a/src/pretix/base/services/checkin.py b/src/pretix/base/services/checkin.py index e106e2e072..903f529d26 100644 --- a/src/pretix/base/services/checkin.py +++ b/src/pretix/base/services/checkin.py @@ -1,6 +1,7 @@ from datetime import timedelta import dateutil +from django.core.files import File from django.db import transaction from django.db.models.functions import TruncDate from django.dispatch import receiver @@ -125,6 +126,14 @@ def _save_answers(op, answers, given_answers): else: qa = op.answers.create(question=q, answer=", ".join([str(o) for o in 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: if q in answers: qa = answers[q] diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py index 367ef0f02b..c6e4864596 100644 --- a/src/tests/api/test_checkin.py +++ b/src/tests/api/test_checkin.py @@ -7,6 +7,7 @@ from unittest import mock from urllib.parse import quote as urlquote import pytest +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 @@ -995,3 +996,46 @@ def test_question_multiple_choice(token_client, organizer, clist, event, order, with scopes_disabled(): 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]} + + +@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