diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 8dd21497cb..a46066bb48 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -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 = [ diff --git a/src/pretix/api/serializers/fields.py b/src/pretix/api/serializers/fields.py index f23a7ad82d..01440cb9b8 100644 --- a/src/pretix/api/serializers/fields.py +++ b/src/pretix/api/serializers/fields.py @@ -1,5 +1,6 @@ from collections import OrderedDict +from django.core.exceptions import ValidationError from rest_framework import serializers @@ -52,6 +53,8 @@ class UploadedFileField(serializers.Field): file__isnull=False, pk=data[len("file:"):], ) + except (ValidationError, IndexError): # invalid uuid + self.fail('not_found') except CachedFile.DoesNotExist: self.fail('not_found') @@ -70,7 +73,5 @@ class UploadedFileField(serializers.Field): url = value.url except AttributeError: return None - request = self.context.get('request', None) - if request is not None: - return request.build_absolute_uri(url) - return url + request = self.context['request'] + return request.build_absolute_uri(url) diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index c4d65c5739..b1189bf045 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -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 diff --git a/src/pretix/api/serializers/settings.py b/src/pretix/api/serializers/settings.py new file mode 100644 index 0000000000..a4387798ef --- /dev/null +++ b/src/pretix/api/serializers/settings.py @@ -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() diff --git a/src/pretix/api/urls.py b/src/pretix/api/urls.py index 6737a90ba6..5875323fed 100644 --- a/src/pretix/api/urls.py +++ b/src/pretix/api/urls.py @@ -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, - upload, version, voucher, waitinglist, webhooks, + checkin, device, event, exporters, item, oauth, order, organizer, upload, + user, version, voucher, waitinglist, webhooks, ) router = routers.DefaultRouter() diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py index f6037ca5b3..e7f1da7adb 100644 --- a/src/pretix/api/views/event.py +++ b/src/pretix/api/views/event.py @@ -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() @@ -393,5 +397,8 @@ 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) + s = EventSettingsSerializer( + instance=request.event.settings, event=request.event, context={ + 'request': request + }) return Response(s.data) diff --git a/src/pretix/api/views/organizer.py b/src/pretix/api/views/organizer.py index a87fcfa48a..bafab9131f 100644 --- a/src/pretix/api/views/organizer.py +++ b/src/pretix/api/views/organizer.py @@ -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) diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index e3e339ea46..9588a387f0 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -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': '', diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 8fcf6d90d6..540b401dea 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -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') diff --git a/src/tests/api/test_events.py b/src/tests/api/test_events.py index 5e61cd2804..f3661b3f98 100644 --- a/src/tests/api/test_events.py +++ b/src/tests/api/test_events.py @@ -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 diff --git a/src/tests/api/test_organizers.py b/src/tests/api/test_organizers.py index f13f25cafb..e8f984bfbb 100644 --- a/src/tests/api/test_organizers.py +++ b/src/tests/api/test_organizers.py @@ -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