forked from CGM_Public/pretix_original
API: Allow to answer file upload questions during ticket redemption
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user