mirror of
https://github.com/pretix/pretix.git
synced 2026-04-26 23:52:35 +00:00
Merge pull request #1901 from pretix/api-upload
This commit is contained in:
@@ -183,6 +183,9 @@ Relative date *either* String in ISO 8601 ``"2017-12-27"``,
|
||||
constructed from a number of
|
||||
days before the base point
|
||||
and the base point.
|
||||
File URL in responses, ``file:`` ``"https://…"``, ``"file:…"``
|
||||
specifiers in requests
|
||||
(see below).
|
||||
===================== ============================ ===================================
|
||||
|
||||
Query parameters
|
||||
@@ -227,4 +230,48 @@ We store idempotency keys for 24 hours, so you should never retry a request afte
|
||||
All ``POST``, ``PUT``, ``PATCH``, or ``DELETE`` api calls support idempotency keys. Adding an idempotency key to a
|
||||
``GET``, ``HEAD``, or ``OPTIONS`` request has no effect.
|
||||
|
||||
|
||||
File upload
|
||||
-----------
|
||||
|
||||
In some places, the API supports working with files, for example when setting the picture of a product. In this case,
|
||||
you will first need to make a separate request to our file upload endpoint:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/upload HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Authorization: Token e1l6gq2ye72thbwkacj7jbri7a7tvxe614ojv8ybureain92ocub46t5gab5966k
|
||||
Content-Type: image/png
|
||||
Content-Disposition: attachment; filename="logo.png"
|
||||
Content-Length: 1234
|
||||
|
||||
<raw file content>
|
||||
|
||||
Note that the ``Content-Type`` and ``Content-Disposition`` headers are required. If the upload was successful, you will
|
||||
receive a JSON response with the ID of the file:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "file:1cd99455-1ebd-4cda-b1a2-7a7d2a969ad1"
|
||||
}
|
||||
|
||||
You can then use this file ID in the request you want to use it in. File IDs are currently valid for 24 hours and can only
|
||||
be used using the same authorization method and user that was used to upload them.
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/test/events/test/items/3/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"picture": "file:1cd99455-1ebd-4cda-b1a2-7a7d2a969ad1"
|
||||
}
|
||||
|
||||
|
||||
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax
|
||||
|
||||
@@ -36,8 +36,8 @@ admission boolean ``true`` for it
|
||||
(such as primary tickets) and ``false`` for others
|
||||
(such as add-ons or merchandise).
|
||||
position integer An integer, used for sorting
|
||||
picture string A product picture to be displayed in the shop
|
||||
(read-only, can be ``null``).
|
||||
picture file A product picture to be displayed in the shop
|
||||
(can be ``null``).
|
||||
sales_channels list of strings Sales channels this product is available on, such as
|
||||
``"web"`` or ``"resellers"``. Defaults to ``["web"]``.
|
||||
available_from datetime The first date time at which this item can be bought
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext as _
|
||||
from django_countries.serializers import CountryFieldMixin
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from pytz import common_timezones
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import ChoiceField, Field
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.models import Event, TaxRule
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||
from pretix.base.services.seating import (
|
||||
SeatProtected, generate_seats, validate_plan_change,
|
||||
)
|
||||
from pretix.base.settings import DEFAULTS, validate_event_settings
|
||||
from pretix.base.settings import validate_event_settings
|
||||
from pretix.base.signals import api_event_settings_fields
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MetaDataField(Field):
|
||||
|
||||
@@ -558,7 +562,7 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
|
||||
|
||||
|
||||
class EventSettingsSerializer(serializers.Serializer):
|
||||
class EventSettingsSerializer(SettingsSerializer):
|
||||
default_fields = [
|
||||
'imprint_url',
|
||||
'checkout_email_helptext',
|
||||
@@ -654,6 +658,7 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'invoice_additional_text',
|
||||
'invoice_footer_text',
|
||||
'invoice_eu_currencies',
|
||||
'invoice_logo_image',
|
||||
'cancel_allow_user',
|
||||
'cancel_allow_user_until',
|
||||
'cancel_allow_user_paid',
|
||||
@@ -674,45 +679,21 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
'theme_color_background',
|
||||
'theme_round_borders',
|
||||
'primary_font',
|
||||
'logo_image',
|
||||
'logo_image_large',
|
||||
'logo_show_title',
|
||||
'og_image',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
self.changed_data = []
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.default_fields:
|
||||
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||
if callable(kwargs):
|
||||
kwargs = kwargs()
|
||||
kwargs.setdefault('required', False)
|
||||
kwargs.setdefault('allow_null', True)
|
||||
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
if callable(form_kwargs):
|
||||
form_kwargs = form_kwargs()
|
||||
if 'serializer_class' not in DEFAULTS[fname]:
|
||||
raise ValidationError('{} has no serializer class'.format(fname))
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
**kwargs
|
||||
)
|
||||
f._label = form_kwargs.get('label', fname)
|
||||
f._help_text = form_kwargs.get('help_text')
|
||||
self.fields[fname] = f
|
||||
|
||||
for recv, resp in api_event_settings_fields.send(sender=self.event):
|
||||
for fname, field in resp.items():
|
||||
field.required = False
|
||||
self.fields[fname] = field
|
||||
|
||||
def update(self, instance: HierarkeyProxy, validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
if value is None:
|
||||
instance.delete(attr)
|
||||
self.changed_data.append(attr)
|
||||
elif instance.get(attr, as_type=type(value)) != value:
|
||||
instance.set(attr, value)
|
||||
self.changed_data.append(attr)
|
||||
return instance
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
settings_dict = self.instance.freeze()
|
||||
@@ -720,6 +701,14 @@ class EventSettingsSerializer(serializers.Serializer):
|
||||
validate_event_settings(self.event, settings_dict)
|
||||
return data
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
nonce = get_random_string(length=8)
|
||||
fname = '%s/%s/%s.%s.%s' % (
|
||||
self.event.organizer.slug, self.event.slug, name.split('/')[-1], nonce, name.split('.')[-1]
|
||||
)
|
||||
# TODO: make sure pub is always correct
|
||||
return 'pub/' + fname
|
||||
|
||||
|
||||
class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
default_fields = [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
@@ -27,3 +28,50 @@ class ListMultipleChoiceField(serializers.MultipleChoiceField):
|
||||
]
|
||||
|
||||
return remove_duplicates_from_list(representation_data)
|
||||
|
||||
|
||||
class UploadedFileField(serializers.Field):
|
||||
default_error_messages = {
|
||||
'required': 'No file was submitted.',
|
||||
'not_found': 'The submitted file ID was not found.',
|
||||
'invalid_type': 'The submitted file has a file type that is not allowed in this field.',
|
||||
'size': 'The submitted file is too large to be used in this field.',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.allowed_types = kwargs.pop('allowed_types', None)
|
||||
self.max_size = kwargs.pop('max_size', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
from pretix.base.models import CachedFile
|
||||
|
||||
request = self.context.get('request', None)
|
||||
try:
|
||||
cf = CachedFile.objects.get(
|
||||
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}',
|
||||
file__isnull=False,
|
||||
pk=data[len("file:"):],
|
||||
)
|
||||
except (ValidationError, IndexError): # invalid uuid
|
||||
self.fail('not_found')
|
||||
except CachedFile.DoesNotExist:
|
||||
self.fail('not_found')
|
||||
|
||||
if self.allowed_types and cf.type not in self.allowed_types:
|
||||
self.fail('invalid_type')
|
||||
if self.max_size and cf.file.size > self.max_size:
|
||||
self.fail('size')
|
||||
|
||||
return cf.file
|
||||
|
||||
def to_representation(self, value):
|
||||
if not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
url = value.url
|
||||
except AttributeError:
|
||||
return None
|
||||
request = self.context['request']
|
||||
return request.build_absolute_uri(url)
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.event import MetaDataField
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
|
||||
@@ -113,6 +114,9 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
variations = InlineItemVariationSerializer(many=True, required=False)
|
||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||
meta_data = MetaDataField(required=False, source='*')
|
||||
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
), max_size=10 * 1024 * 1024)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
@@ -123,7 +127,7 @@ class ItemSerializer(I18nAwareModelSerializer):
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
|
||||
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
|
||||
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data')
|
||||
read_only_fields = ('has_variations', 'picture')
|
||||
read_only_fields = ('has_variations',)
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.i18n import get_language_without_region
|
||||
from pretix.base.models import (
|
||||
@@ -16,9 +18,11 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.settings import DEFAULTS, validate_organizer_settings
|
||||
from pretix.base.settings import validate_organizer_settings
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
@@ -207,7 +211,7 @@ class TeamMemberSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
class OrganizerSettingsSerializer(serializers.Serializer):
|
||||
class OrganizerSettingsSerializer(SettingsSerializer):
|
||||
default_fields = [
|
||||
'organizer_info_text',
|
||||
'event_list_type',
|
||||
@@ -225,40 +229,13 @@ class OrganizerSettingsSerializer(serializers.Serializer):
|
||||
'theme_color_danger',
|
||||
'theme_color_background',
|
||||
'theme_round_borders',
|
||||
'primary_font'
|
||||
'primary_font',
|
||||
'organizer_logo_image'
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.organizer = kwargs.pop('organizer')
|
||||
self.changed_data = []
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.default_fields:
|
||||
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||
if callable(kwargs):
|
||||
kwargs = kwargs()
|
||||
kwargs.setdefault('required', False)
|
||||
kwargs.setdefault('allow_null', True)
|
||||
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
if callable(form_kwargs):
|
||||
form_kwargs = form_kwargs()
|
||||
if 'serializer_class' not in DEFAULTS[fname]:
|
||||
raise ValidationError('{} has no serializer class'.format(fname))
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
**kwargs
|
||||
)
|
||||
f._label = form_kwargs.get('label', fname)
|
||||
f._help_text = form_kwargs.get('help_text')
|
||||
self.fields[fname] = f
|
||||
|
||||
def update(self, instance: HierarkeyProxy, validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
if value is None:
|
||||
instance.delete(attr)
|
||||
self.changed_data.append(attr)
|
||||
elif instance.get(attr, as_type=type(value)) != value:
|
||||
instance.set(attr, value)
|
||||
self.changed_data.append(attr)
|
||||
return instance
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
@@ -266,3 +243,11 @@ class OrganizerSettingsSerializer(serializers.Serializer):
|
||||
settings_dict.update(data)
|
||||
validate_organizer_settings(self.organizer, settings_dict)
|
||||
return data
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
nonce = get_random_string(length=8)
|
||||
fname = '%s/%s.%s.%s' % (
|
||||
self.organizer.slug, name.split('/')[-1], nonce, name.split('.')[-1]
|
||||
)
|
||||
# TODO: make sure pub is always correct
|
||||
return 'pub/' + fname
|
||||
|
||||
77
src/pretix/api/serializers/settings.py
Normal file
77
src/pretix/api/serializers/settings.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import logging
|
||||
|
||||
from django.core.files import File
|
||||
from django.core.files.storage import default_storage
|
||||
from django.db.models.fields.files import FieldFile
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.serializers.fields import UploadedFileField
|
||||
from pretix.base.settings import DEFAULTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettingsSerializer(serializers.Serializer):
|
||||
default_fields = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.changed_data = []
|
||||
super().__init__(*args, **kwargs)
|
||||
for fname in self.default_fields:
|
||||
kwargs = DEFAULTS[fname].get('serializer_kwargs', {})
|
||||
if callable(kwargs):
|
||||
kwargs = kwargs()
|
||||
kwargs.setdefault('required', False)
|
||||
kwargs.setdefault('allow_null', True)
|
||||
form_kwargs = DEFAULTS[fname].get('form_kwargs', {})
|
||||
if callable(form_kwargs):
|
||||
form_kwargs = form_kwargs()
|
||||
if 'serializer_class' not in DEFAULTS[fname]:
|
||||
raise ValidationError('{} has no serializer class'.format(fname))
|
||||
f = DEFAULTS[fname]['serializer_class'](
|
||||
**kwargs
|
||||
)
|
||||
f._label = form_kwargs.get('label', fname)
|
||||
f._help_text = form_kwargs.get('help_text')
|
||||
f.parent = self
|
||||
self.fields[fname] = f
|
||||
|
||||
def update(self, instance: HierarkeyProxy, validated_data):
|
||||
for attr, value in validated_data.items():
|
||||
if isinstance(value, FieldFile):
|
||||
# Delete old file
|
||||
fname = instance.get(attr, as_type=File)
|
||||
if fname:
|
||||
try:
|
||||
default_storage.delete(fname.name)
|
||||
except OSError: # pragma: no cover
|
||||
logger.error('Deleting file %s failed.' % fname.name)
|
||||
|
||||
# Create new file
|
||||
newname = default_storage.save(self.get_new_filename(value.name), value)
|
||||
instance.set(attr, File(file=value, name=newname))
|
||||
self.changed_data.append(attr)
|
||||
elif isinstance(self.fields[attr], UploadedFileField):
|
||||
if value is None:
|
||||
fname = instance.get(attr, as_type=File)
|
||||
if fname:
|
||||
try:
|
||||
default_storage.delete(fname.name)
|
||||
except OSError: # pragma: no cover
|
||||
logger.error('Deleting file %s failed.' % fname.name)
|
||||
instance.delete(attr)
|
||||
else:
|
||||
# file is unchanged
|
||||
continue
|
||||
elif value is None:
|
||||
instance.delete(attr)
|
||||
self.changed_data.append(attr)
|
||||
elif instance.get(attr, as_type=type(value)) != value:
|
||||
instance.set(attr, value)
|
||||
self.changed_data.append(attr)
|
||||
return instance
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
raise NotImplementedError()
|
||||
@@ -7,8 +7,8 @@ from rest_framework import routers
|
||||
from pretix.api.views import cart
|
||||
|
||||
from .views import (
|
||||
checkin, device, event, exporters, item, oauth, order, organizer, user,
|
||||
version, voucher, waitinglist, webhooks,
|
||||
checkin, device, event, exporters, item, oauth, order, organizer, upload,
|
||||
user, version, voucher, waitinglist, webhooks,
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
@@ -95,6 +95,7 @@ urlpatterns = [
|
||||
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
||||
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
||||
url(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
|
||||
url(r"^upload$", upload.UploadView.as_view(), name="user.me"),
|
||||
url(r"^me$", user.MeView.as_view(), name="user.me"),
|
||||
url(r"^version$", version.VersionView.as_view(), name="version"),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -365,9 +365,13 @@ class EventSettingsView(views.APIView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if isinstance(request.auth, Device):
|
||||
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
})
|
||||
elif 'can_change_event_settings' in request.eventpermset:
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
})
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
if 'explain' in request.GET:
|
||||
@@ -382,7 +386,7 @@ class EventSettingsView(views.APIView):
|
||||
|
||||
def patch(self, request, *wargs, **kwargs):
|
||||
s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True,
|
||||
event=request.event)
|
||||
event=request.event, context={'request': request})
|
||||
s.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
s.save()
|
||||
@@ -392,6 +396,9 @@ class EventSettingsView(views.APIView):
|
||||
}
|
||||
)
|
||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_css.apply_async(args=(request.organizer.pk,))
|
||||
s = EventSettingsSerializer(instance=request.event.settings, event=request.event)
|
||||
regenerate_css.apply_async(args=(request.event.pk,))
|
||||
s = EventSettingsSerializer(
|
||||
instance=request.event.settings, event=request.event, context={
|
||||
'request': request
|
||||
})
|
||||
return Response(s.data)
|
||||
|
||||
@@ -425,7 +425,9 @@ class OrganizerSettingsView(views.APIView):
|
||||
permission = 'can_change_organizer_settings'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer)
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||
'request': request
|
||||
})
|
||||
if 'explain' in request.GET:
|
||||
return Response({
|
||||
fname: {
|
||||
@@ -439,7 +441,9 @@ class OrganizerSettingsView(views.APIView):
|
||||
def patch(self, request, *wargs, **kwargs):
|
||||
s = OrganizerSettingsSerializer(
|
||||
instance=request.organizer.settings, data=request.data, partial=True,
|
||||
organizer=request.organizer
|
||||
organizer=request.organizer, context={
|
||||
'request': request
|
||||
}
|
||||
)
|
||||
s.is_valid(raise_exception=True)
|
||||
with transaction.atomic():
|
||||
@@ -451,5 +455,7 @@ class OrganizerSettingsView(views.APIView):
|
||||
)
|
||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
||||
regenerate_organizer_css.apply_async(args=(request.organizer.pk,))
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer)
|
||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||
'request': request
|
||||
})
|
||||
return Response(s.data)
|
||||
|
||||
53
src/pretix/api/views/upload.py
Normal file
53
src/pretix/api/views/upload.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import datetime
|
||||
|
||||
from django.utils.timezone import now
|
||||
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.parsers import FileUploadParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||
from pretix.api.auth.token import TeamTokenAuthentication
|
||||
from pretix.base.models import CachedFile
|
||||
|
||||
ALLOWED_TYPES = {
|
||||
'image/gif': {'.gif'},
|
||||
'image/jpeg': {'.jpg', '.jpeg'},
|
||||
'image/png': {'.png'},
|
||||
'application/pdf': {'.pdf'},
|
||||
}
|
||||
|
||||
|
||||
class UploadView(APIView):
|
||||
authentication_classes = (
|
||||
SessionAuthentication, OAuth2Authentication, DeviceTokenAuthentication, TeamTokenAuthentication
|
||||
)
|
||||
parser_classes = [FileUploadParser]
|
||||
|
||||
def post(self, request):
|
||||
if 'file' not in request.data:
|
||||
raise ValidationError('No file has been submitted.')
|
||||
file_obj = request.data['file']
|
||||
content_type = file_obj.content_type.split(";")[0] # ignore e.g. "; charset=…"
|
||||
if content_type not in ALLOWED_TYPES:
|
||||
raise ValidationError('Content type "{type}" is not allowed'.format(type=content_type))
|
||||
if not any(file_obj.name.endswith(ext) for ext in ALLOWED_TYPES[content_type]):
|
||||
raise ValidationError('File name "{name}" has an invalid extension for type "{type}"'.format(
|
||||
name=file_obj.name,
|
||||
type=content_type
|
||||
))
|
||||
cf = CachedFile.objects.create(
|
||||
expires=now() + datetime.timedelta(days=1),
|
||||
date=now(),
|
||||
web_download=False,
|
||||
filename=file_obj.name,
|
||||
type=content_type,
|
||||
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}'
|
||||
)
|
||||
cf.file.save(file_obj.name, file_obj)
|
||||
cf.save()
|
||||
return Response({
|
||||
'id': f'file:{cf.pk}'
|
||||
}, status=201)
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -21,7 +21,9 @@ from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.fields import ListMultipleChoiceField
|
||||
from pretix.api.serializers.fields import (
|
||||
ListMultipleChoiceField, UploadedFileField,
|
||||
)
|
||||
from pretix.api.serializers.i18n import I18nField
|
||||
from pretix.base.models.tax import TaxRule
|
||||
from pretix.base.reldate import (
|
||||
@@ -29,7 +31,7 @@ from pretix.base.reldate import (
|
||||
SerializerRelativeDateField, SerializerRelativeDateTimeField,
|
||||
)
|
||||
from pretix.control.forms import (
|
||||
FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
|
||||
ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
|
||||
)
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
|
||||
@@ -1784,19 +1786,66 @@ Your {event} team"""))
|
||||
},
|
||||
'logo_image': {
|
||||
'default': None,
|
||||
'type': File
|
||||
'type': File,
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('If you provide a logo image, we will by default not show your event name and date '
|
||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||
'can increase the size with the setting below. We recommend not using small details on the picture '
|
||||
'as it will be resized on smaller screens.')
|
||||
),
|
||||
'serializer_class': UploadedFileField,
|
||||
'serializer_kwargs': dict(
|
||||
allowed_types=[
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
],
|
||||
max_size=10 * 1024 * 1024,
|
||||
)
|
||||
|
||||
},
|
||||
'logo_image_large': {
|
||||
'default': 'False',
|
||||
'type': bool
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Use header image in its full size'),
|
||||
help_text=_('We recommend to upload a picture at least 1170 pixels wide.'),
|
||||
)
|
||||
},
|
||||
'logo_show_title': {
|
||||
'default': 'True',
|
||||
'type': bool
|
||||
'type': bool,
|
||||
'form_class': forms.BooleanField,
|
||||
'serializer_class': serializers.BooleanField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Show event title even if a header image is present'),
|
||||
help_text=_('The title will only be shown on the event front page.'),
|
||||
)
|
||||
},
|
||||
'organizer_logo_image': {
|
||||
'default': None,
|
||||
'type': File
|
||||
'type': File,
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||
'can increase the size with the setting below. We recommend not using small details on the picture '
|
||||
'as it will be resized on smaller screens.')
|
||||
),
|
||||
'serializer_class': UploadedFileField,
|
||||
'serializer_kwargs': dict(
|
||||
allowed_types=[
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
],
|
||||
max_size=10 * 1024 * 1024,
|
||||
)
|
||||
},
|
||||
'organizer_logo_image_large': {
|
||||
'default': 'False',
|
||||
@@ -1810,11 +1859,43 @@ Your {event} team"""))
|
||||
},
|
||||
'og_image': {
|
||||
'default': None,
|
||||
'type': File
|
||||
'type': File,
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Social media image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
|
||||
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
|
||||
'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good '
|
||||
'only the center square is shown. If you do not fill this, we will use the logo given above.')
|
||||
),
|
||||
'serializer_class': UploadedFileField,
|
||||
'serializer_kwargs': dict(
|
||||
allowed_types=[
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
],
|
||||
max_size=10 * 1024 * 1024,
|
||||
)
|
||||
},
|
||||
'invoice_logo_image': {
|
||||
'default': None,
|
||||
'type': File
|
||||
'type': File,
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
|
||||
),
|
||||
'serializer_class': UploadedFileField,
|
||||
'serializer_kwargs': dict(
|
||||
allowed_types=[
|
||||
'image/png', 'image/jpeg', 'image/gif'
|
||||
],
|
||||
max_size=10 * 1024 * 1024,
|
||||
)
|
||||
},
|
||||
'frontpage_text': {
|
||||
'default': '',
|
||||
|
||||
@@ -27,7 +27,7 @@ from pretix.base.settings import (
|
||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_event_settings,
|
||||
)
|
||||
from pretix.control.forms import (
|
||||
ExtFileField, MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
|
||||
MultipleLanguagesWidget, SlugWidget, SplitDateTimeField,
|
||||
SplitDateTimePickerWidget,
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2
|
||||
@@ -416,36 +416,6 @@ class EventSettingsForm(SettingsForm):
|
||||
"restrict the set of selectable titles."),
|
||||
required=False,
|
||||
)
|
||||
logo_image = ExtFileField(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('If you provide a logo image, we will by default not show your event name and date '
|
||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||
'can increase the size with the setting below. We recommend not using small details on the picture '
|
||||
'as it will be resized on smaller screens.')
|
||||
)
|
||||
logo_image_large = forms.BooleanField(
|
||||
label=_('Use header image in its full size'),
|
||||
help_text=_('We recommend to upload a picture at least 1170 pixels wide.'),
|
||||
required=False,
|
||||
)
|
||||
logo_show_title = forms.BooleanField(
|
||||
label=_('Show event title even if a header image is present'),
|
||||
help_text=_('The title will only be shown on the event front page.'),
|
||||
required=False,
|
||||
)
|
||||
og_image = ExtFileField(
|
||||
label=_('Social media image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
|
||||
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
|
||||
'WhatsApp and Reddit only show a square preview, so we recommend to make sure it still looks good '
|
||||
'only the center square is shown. If you do not fill this, we will use the logo given above.')
|
||||
)
|
||||
|
||||
auto_fields = [
|
||||
'imprint_url',
|
||||
@@ -498,6 +468,10 @@ class EventSettingsForm(SettingsForm):
|
||||
'theme_color_background',
|
||||
'theme_round_borders',
|
||||
'primary_font',
|
||||
'logo_image',
|
||||
'logo_image_large',
|
||||
'logo_show_title',
|
||||
'og_image',
|
||||
]
|
||||
|
||||
def clean(self):
|
||||
@@ -733,6 +707,7 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
'invoice_additional_text',
|
||||
'invoice_footer_text',
|
||||
'invoice_eu_currencies',
|
||||
'invoice_logo_image',
|
||||
]
|
||||
|
||||
invoice_generate_sales_channels = forms.MultipleChoiceField(
|
||||
@@ -752,13 +727,6 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
label=_("Invoice language"),
|
||||
choices=[('__user__', _('The user\'s language'))] + settings.LANGUAGES,
|
||||
)
|
||||
invoice_logo_image = ExtFileField(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
max_size=10 * 1024 * 1024,
|
||||
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs.get('obj')
|
||||
|
||||
@@ -344,6 +344,11 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
),
|
||||
'TEST_REQUEST_RENDERER_CLASSES': [
|
||||
'rest_framework.renderers.MultiPartRenderer',
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'pretix.testutils.api.UploadRenderer',
|
||||
],
|
||||
'EXCEPTION_HANDLER': 'pretix.api.exception.custom_exception_handler',
|
||||
'UNICODE_JSON': False
|
||||
}
|
||||
|
||||
11
src/pretix/testutils/api.py
Normal file
11
src/pretix/testutils/api.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework.renderers import BaseRenderer
|
||||
|
||||
|
||||
class UploadRenderer(BaseRenderer):
|
||||
media_type = None
|
||||
format = 'upload'
|
||||
charset = 'utf-8'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
self.media_type = data['media_type']
|
||||
return data['file']
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scopes_disabled
|
||||
from pytz import UTC
|
||||
@@ -1105,3 +1106,74 @@ def test_patch_event_settings_validation(token_client, organizer, event):
|
||||
assert resp.data == {
|
||||
'cancel_allow_user_until': ['Invalid relative date']
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_patch_event_settings_file(token_client, organizer, event):
|
||||
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']
|
||||
|
||||
r = token_client.post(
|
||||
'/api/v1/upload',
|
||||
data={
|
||||
'media_type': 'application/pdf',
|
||||
'file': ContentFile('file.pdf', 'invalid pdf content')
|
||||
},
|
||||
format='upload',
|
||||
HTTP_CONTENT_DISPOSITION='attachment; filename="file.pdf"',
|
||||
)
|
||||
assert r.status_code == 201
|
||||
file_id_pdf = r.data['id']
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
'logo_image': 'invalid'
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'logo_image': ['The submitted file ID was not found.']
|
||||
}
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
'logo_image': file_id_pdf
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'logo_image': ['The submitted file has a file type that is not allowed in this field.']
|
||||
}
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
'logo_image': file_id_png
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['logo_image'].startswith('http')
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
'logo_image': None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['logo_image'] is None
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from pretix.testutils.mock import mocker_context
|
||||
|
||||
@@ -100,3 +101,74 @@ def test_patch_settings(token_client, organizer):
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
mocked.assert_any_call(args=(organizer.pk,))
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_patch_organizer_settings_file(token_client, organizer):
|
||||
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']
|
||||
|
||||
r = token_client.post(
|
||||
'/api/v1/upload',
|
||||
data={
|
||||
'media_type': 'application/pdf',
|
||||
'file': ContentFile('file.pdf', 'invalid pdf content')
|
||||
},
|
||||
format='upload',
|
||||
HTTP_CONTENT_DISPOSITION='attachment; filename="file.pdf"',
|
||||
)
|
||||
assert r.status_code == 201
|
||||
file_id_pdf = r.data['id']
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/settings/'.format(organizer.slug),
|
||||
{
|
||||
'organizer_logo_image': 'invalid'
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'organizer_logo_image': ['The submitted file ID was not found.']
|
||||
}
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/settings/'.format(organizer.slug),
|
||||
{
|
||||
'organizer_logo_image': file_id_pdf
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'organizer_logo_image': ['The submitted file has a file type that is not allowed in this field.']
|
||||
}
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/settings/'.format(organizer.slug,),
|
||||
{
|
||||
'organizer_logo_image': file_id_png
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['organizer_logo_image'].startswith('http')
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/settings/'.format(organizer.slug),
|
||||
{
|
||||
'organizer_logo_image': None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['organizer_logo_image'] is None
|
||||
|
||||
31
src/tests/api/test_upload.py
Normal file
31
src/tests/api/test_upload.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import pytest
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_upload_file(token_client):
|
||||
r = token_client.post(
|
||||
'/api/v1/upload',
|
||||
data={
|
||||
'media_type': 'application/pdf',
|
||||
'file': ContentFile('file.pdf', 'invalid pdf content')
|
||||
},
|
||||
format='upload',
|
||||
HTTP_CONTENT_DISPOSITION='attachment; filename="file.pdf"',
|
||||
)
|
||||
assert r.status_code == 201
|
||||
assert r.data['id'].startswith('file:')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_upload_file_extension_mismatch(token_client):
|
||||
r = token_client.post(
|
||||
'/api/v1/upload',
|
||||
data={
|
||||
'media_type': 'application/pdf',
|
||||
'file': ContentFile('file.png', 'invalid pdf content')
|
||||
},
|
||||
format='upload',
|
||||
HTTP_CONTENT_DISPOSITION='attachment; filename="file.png"',
|
||||
)
|
||||
assert r.status_code == 400
|
||||
Reference in New Issue
Block a user