diff --git a/doc/development/implementation/settings.rst b/doc/development/implementation/settings.rst index ae59366bbc..9da127ba07 100644 --- a/doc/development/implementation/settings.rst +++ b/doc/development/implementation/settings.rst @@ -2,7 +2,10 @@ Settings storage ================ pretix is highly configurable and therefore needs to store a lot of per-event and per-organizer settings. -Those settings are stored in the database and accessed through a ``SettingsProxy`` instance. You can obtain +For this purpose, we use `django-hierarkey`_ which started out as part of pretix and then got refactored into +its own library. It has a comprehensive `documentation`_ which you should read if you work with settings in pretix. + +The settings are stored in the database and accessed through a ``HierarkeyProxy`` instance. You can obtain such an instance from any event or organizer model instance by just accessing ``event.settings`` or ``organizer.settings``, respectively. @@ -17,12 +20,10 @@ includes serializers for serializing the following types: In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for convenience in templates you can also access settings values at ``settings[name]`` and ``settings.name``. - -.. autoclass:: pretix.base.settings.SettingsProxy - :members: get, set, delete, freeze +See the hierarkey `documentation`_ for more information. To avoid naming conflicts, plugins are requested to prefix all settings they use with the name of the plugin -or something unique, e.g. ``payment.paypal.api_key``. To reduce redundant typing of this prefix, we provide +or something unique, e.g. ``payment_paypal_api_key``. To reduce redundant typing of this prefix, we provide another helper class: .. autoclass:: pretix.base.settings.SettingsSandbox @@ -33,10 +34,10 @@ you will just be passed a sandbox object with a prefix generated from your provi Forms ----- -We also provide a base class for forms that allow the modification of settings: +Hierarkey also provides a base class for forms that allow the modification of settings. pretix contains a +subclass that also adds suport for internationalized fields: .. autoclass:: pretix.base.forms.SettingsForm - :members: save You can simply use it like this:: @@ -51,3 +52,17 @@ You can simply use it like this:: help_text=_("The number of days after placing an order the user has to pay to " "preserve his reservation."), ) + +Defaults in plugins +------------------- + +Plugins can add custom hardcoded defaults in the following way:: + + from pretix.base.settings import settings_hierarkey + + settings_hierarkey.add_default('key', 'value', type) + +Make sure that you include this code in a module that is imported at app loading time. + +.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey +.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/ \ No newline at end of file diff --git a/src/pretix/base/forms/__init__.py b/src/pretix/base/forms/__init__.py index 1d54d76325..8bde46fcba 100644 --- a/src/pretix/base/forms/__init__.py +++ b/src/pretix/base/forms/__init__.py @@ -1,14 +1,10 @@ import logging import i18nfield.forms -from django import forms -from django.core.files import File -from django.core.files.storage import default_storage -from django.core.files.uploadedfile import UploadedFile from django.forms.models import ModelFormMetaclass from django.utils import six from django.utils.crypto import get_random_string -from django.utils.translation import ugettext_lazy as _ +from hierarkey.forms import HierarkeyForm from pretix.base.models import Event @@ -49,67 +45,22 @@ class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet): super().__init__(*args, **kwargs) -class SettingsForm(i18nfield.forms.I18nForm): - """ - This form is meant to be used for modifying EventSettings or OrganizerSettings. It takes - care of loading the current values of the fields and saving the field inputs to the - settings storage. It also deals with setting the available languages for internationalized - fields. - - :param obj: The event or organizer object which should be used for the settings storage - """ - - BOOL_CHOICES = ( - ('False', _('disabled')), - ('True', _('enabled')), - ) +class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm): def __init__(self, *args, **kwargs): - self.obj = kwargs.pop('obj', None) + self.obj = kwargs.get('obj', None) self.locales = kwargs.pop('locales', None) + kwargs['attribute_name'] = 'settings' kwargs['locales'] = self.obj.settings.get('locales') if self.obj else self.locales kwargs['initial'] = self.obj.settings.freeze() super().__init__(*args, **kwargs) - def save(self): - """ - Performs the save operation - """ - for name, field in self.fields.items(): - value = self.cleaned_data[name] - if isinstance(value, UploadedFile): - # Delete old file - fname = self.obj.settings.get(name, as_type=File) - if fname: - try: - default_storage.delete(fname.name) - except OSError: - logger.error('Deleting file %s failed.' % fname.name) - - # Create new file - nonce = get_random_string(length=8) - if isinstance(self.obj, Event): - fname = '%s/%s/%s.%s.%s' % ( - self.obj.organizer.slug, self.obj.slug, name, nonce, value.name.split('.')[-1] - ) - else: - fname = '%s/%s.%s.%s' % (self.obj.slug, name, nonce, value.name.split('.')[-1]) - newname = default_storage.save(fname, value) - value._name = newname - self.obj.settings.set(name, value) - elif isinstance(value, File): - # file is unchanged - continue - elif isinstance(field, forms.FileField): - # file is deleted - fname = self.obj.settings.get(name, as_type=File) - if fname: - try: - default_storage.delete(fname.name) - except OSError: - logger.error('Deleting file %s failed.' % fname.name) - del self.obj.settings[name] - elif value is None: - del self.obj.settings[name] - elif self.obj.settings.get(name, as_type=type(value)) != value: - self.obj.settings.set(name, value) + def get_new_filename(self, name: str) -> str: + nonce = get_random_string(length=8) + if isinstance(self.obj, Event): + fname = '%s/%s/%s.%s.%s' % ( + self.obj.organizer.slug, self.obj.slug, name, nonce, name.split('.')[-1] + ) + else: + fname = '%s/%s.%s.%s' % (self.obj.slug, name, nonce, name.split('.')[-1]) + return fname diff --git a/src/pretix/base/migrations/0053_auto_20170409_1651.py b/src/pretix/base/migrations/0053_auto_20170409_1651.py new file mode 100644 index 0000000000..cef6a4ad06 --- /dev/null +++ b/src/pretix/base/migrations/0053_auto_20170409_1651.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-04-09 16:51 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.db import migrations, models + + +def migrate_global_settings(apps, schema_editor): + GlobalSetting = apps.get_model('pretixbase', 'GlobalSetting') + GlobalSettingsObject_SettingsStore = apps.get_model('pretixbase', 'GlobalSettingsObject_SettingsStore') + + l = [] + for s in GlobalSetting.objects.all(): + l.append(GlobalSettingsObject_SettingsStore(key=s.key, value=s.value)) + + GlobalSettingsObject_SettingsStore.objects.bulk_create(l) + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0052_auto_20170324_1506'), + ] + + operations = [ + migrations.RenameModel( + old_name='EventSetting', + new_name='Event_SettingsStore', + ), + migrations.RenameModel( + old_name='OrganizerSetting', + new_name='Organizer_SettingsStore', + ), + migrations.CreateModel( + name='GlobalSettingsObject_SettingsStore', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(db_index=True, max_length=255)), + ('value', models.TextField()), + ], + ), + migrations.RunPython( + migrate_global_settings, migrations.RunPython.noop + ), + migrations.DeleteModel( + name='GlobalSetting', + ), + migrations.AlterField( + model_name='event_settingsstore', + name='object', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='_settings_objects', to='pretixbase.Event'), + ), + migrations.AlterField( + model_name='organizer_settingsstore', + name='object', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='_settings_objects', to='pretixbase.Organizer'), + ), + ] diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py index 8fc549534e..4695875cbd 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -1,8 +1,9 @@ +from ..settings import GlobalSettingsObject_SettingsStore from .auth import U2FDevice, User from .base import CachedFile, LoggedModel, cachedfile_name from .checkin import Checkin from .event import ( - Event, EventLock, EventPermission, EventSetting, RequiredAction, + Event, Event_SettingsStore, EventLock, EventPermission, RequiredAction, generate_invite_token, ) from .invoices import Invoice, InvoiceLine, invoice_filename @@ -17,6 +18,6 @@ from .orders import ( cachedcombinedticket_name, cachedticket_name, generate_position_secret, generate_secret, ) -from .organizer import Organizer, OrganizerPermission, OrganizerSetting +from .organizer import Organizer, Organizer_SettingsStore, OrganizerPermission from .vouchers import Voucher from .waitinglist import WaitingListEntry diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 116582e650..29f80e49b9 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -11,22 +11,21 @@ from django.core.validators import RegexValidator from django.db import models from django.template.defaultfilters import date as _date from django.utils.crypto import get_random_string -from django.utils.functional import cached_property from django.utils.timezone import make_aware, now from django.utils.translation import ugettext_lazy as _ from i18nfield.fields import I18nCharField from pretix.base.email import CustomSMTPBackend from pretix.base.models.base import LoggedModel -from pretix.base.settings import SettingsProxy from pretix.base.validators import EventSlugBlacklistValidator from pretix.helpers.daterange import daterange +from ..settings import settings_hierarkey from .auth import User from .organizer import Organizer -from .settings import EventSetting +@settings_hierarkey.add(parent_field='organizer', cache_namespace='event') class Event(LoggedModel): """ This model represents an event. An event is anything you can buy @@ -183,17 +182,6 @@ class Event(LoggedModel): return ObjectRelatedCache(self) - @cached_property - def settings(self) -> SettingsProxy: - """ - Returns an object representing this event's settings. - """ - try: - return SettingsProxy(self, type=EventSetting, parent=self.organizer) - except Organizer.DoesNotExist: - # Should only happen when creating new events - return SettingsProxy(self, type=EventSetting) - @property def presale_has_ended(self): if self.presale_end and now() > self.presale_end: @@ -290,7 +278,7 @@ class Event(LoggedModel): o.question = q o.save() - for s in EventSetting.objects.filter(object=other): + for s in other.settings._objects.all(): s.object = self s.pk = None if s.value.startswith('file://'): diff --git a/src/pretix/base/models/organizer.py b/src/pretix/base/models/organizer.py index a4c3f426b3..a917e7f34d 100644 --- a/src/pretix/base/models/organizer.py +++ b/src/pretix/base/models/organizer.py @@ -3,17 +3,16 @@ import string from django.core.validators import RegexValidator from django.db import models from django.utils.crypto import get_random_string -from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from pretix.base.models.base import LoggedModel -from pretix.base.settings import SettingsProxy from pretix.base.validators import OrganizerSlugBlacklistValidator +from ..settings import settings_hierarkey from .auth import User -from .settings import OrganizerSetting +@settings_hierarkey.add(cache_namespace='organizer') class Organizer(LoggedModel): """ This model represents an entity organizing events, e.g. a company, institution, @@ -59,14 +58,6 @@ class Organizer(LoggedModel): self.get_cache().clear() return obj - @cached_property - def settings(self) -> SettingsProxy: - """ - Returns an object representing this organizer's settings - """ - from pretix.base.settings import GlobalSettingsObject - return SettingsProxy(self, type=OrganizerSetting, parent=GlobalSettingsObject()) - def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache": """ Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to diff --git a/src/pretix/base/models/settings.py b/src/pretix/base/models/settings.py deleted file mode 100644 index dbddabce17..0000000000 --- a/src/pretix/base/models/settings.py +++ /dev/null @@ -1,34 +0,0 @@ -from django.db import models - - -class GlobalSetting(models.Model): - """ - A global setting is a key-value setting which can be set for a - pretix instance. It will be inherited by all events and organizers. - It is filled via the register_global_settings signal. - """ - key = models.CharField(max_length=255, primary_key=True) - value = models.TextField() - - def __init__(self, *args, object=None, **kwargs): - super().__init__(*args, **kwargs) - - -class OrganizerSetting(models.Model): - """ - An organizer setting is a key-value setting which can be set for an - organizer. It will be inherited by the events of this organizer - """ - object = models.ForeignKey('Organizer', related_name='setting_objects', on_delete=models.CASCADE) - key = models.CharField(max_length=255) - value = models.TextField() - - -class EventSetting(models.Model): - """ - An event setting is a key-value setting which can be set for a - specific event - """ - object = models.ForeignKey('Event', related_name='setting_objects', on_delete=models.CASCADE) - key = models.CharField(max_length=255) - value = models.TextField() diff --git a/src/pretix/base/services/waitinglist.py b/src/pretix/base/services/waitinglist.py index afe5adf74d..62e34ce7ef 100644 --- a/src/pretix/base/services/waitinglist.py +++ b/src/pretix/base/services/waitinglist.py @@ -54,7 +54,7 @@ def assign_automatically(event_id: int, user_id: int=None): @receiver(signal=periodic_task) def process_waitinglist(sender, **kwargs): - qs = Event.objects.prefetch_related('setting_objects', 'organizer__setting_objects').select_related('organizer') + qs = Event.objects.prefetch_related('_settings_objects', 'organizer___settings_objects').select_related('organizer') for e in qs: if e.settings.waiting_list_enabled and e.settings.waiting_list_auto: assign_automatically.apply_async(args=(e.pk,)) diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 341526e3fe..8649d768ac 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -1,19 +1,14 @@ import decimal import json -from datetime import date, datetime, time +from datetime import datetime -from django.core.cache import cache -from typing import Any, Dict, Optional - -import dateutil.parser from django.conf import settings from django.core.files import File -from django.core.files.storage import default_storage from django.db.models import Model from django.utils.translation import ugettext_noop - +from hierarkey.models import GlobalSettingsBase, Hierarkey from i18nfield.strings import LazyI18nString -from pretix.base.models.settings import GlobalSetting +from typing import Any DEFAULTS = { 'max_items_per_order': { @@ -382,194 +377,27 @@ Your {event} team""")) } } +settings_hierarkey = Hierarkey(attribute_name='settings') -class SettingsProxy: - """ - This object allows convenient access to settings stored in the - EventSettings/OrganizerSettings database model. It exposes all settings as - properties and it will do all the nasty inheritance and defaults stuff for - you. - """ +for k, v in DEFAULTS.items(): + settings_hierarkey.add_default(k, v['default'], v['type']) - def __init__(self, obj: Model, parent: Optional[Model]=None, type=None): - self._obj = obj - self._parent = parent - self._cached_obj = None - self._write_cached_obj = None - self._type = type - def _cache(self) -> Dict[str, Any]: - if self._cached_obj is None: - self._cached_obj = cache.get_or_set( - 'settings_{}_{}'.format(self._obj.settings_namespace, self._obj.pk), - lambda: {s.key: s.value for s in self._obj.setting_objects.all()}, - timeout=1800 - ) - return self._cached_obj +def i18n_uns(v): + try: + return LazyI18nString(json.loads(v)) + except ValueError: + return LazyI18nString(str(v)) - def _write_cache(self) -> Dict[str, Any]: - if self._write_cached_obj is None: - self._write_cached_obj = { - s.key: s for s in self._obj.setting_objects.all() - } - return self._write_cached_obj - def _flush(self) -> None: - self._cached_obj = None - self._write_cached_obj = None - self._flush_external_cache() +settings_hierarkey.add_type(LazyI18nString, + serialize=lambda s: json.dumps(s.data), + unserialize=i18n_uns) - def _flush_external_cache(self): - cache.delete('settings_{}_{}'.format(self._obj.settings_namespace, self._obj.pk)) - def freeze(self) -> dict: - """ - Returns a dictionary of all settings set for this object, including - any default values of its parents or hardcoded in pretix. - """ - settings = {} - for key, v in DEFAULTS.items(): - settings[key] = self._unserialize(v['default'], v['type']) - if self._parent: - settings.update(self._parent.settings.freeze()) - for key in self._cache(): - settings[key] = self.get(key) - return settings - - def _unserialize(self, value: str, as_type: type, binary_file=False) -> Any: - if as_type is None and value is not None and value.startswith('file://'): - as_type = File - - if as_type is not None and isinstance(value, as_type): - return value - elif value is None: - return None - elif as_type == int or as_type == float or as_type == decimal.Decimal: - return as_type(value) - elif as_type == dict or as_type == list: - return json.loads(value) - elif as_type == bool or value in ('True', 'False'): - return value == 'True' - elif as_type == File: - try: - fi = default_storage.open(value[7:], 'rb' if binary_file else 'r') - fi.url = default_storage.url(value[7:]) - return fi - except OSError: - return False - elif as_type == datetime: - return dateutil.parser.parse(value) - elif as_type == date: - return dateutil.parser.parse(value).date() - elif as_type == time: - return dateutil.parser.parse(value).time() - elif as_type == LazyI18nString and not isinstance(value, LazyI18nString): - try: - return LazyI18nString(json.loads(value)) - except ValueError: - return LazyI18nString(str(value)) - elif as_type is not None and issubclass(as_type, Model): - return as_type.objects.get(pk=value) - return value - - def _serialize(self, value: Any) -> str: - if isinstance(value, str): - return value - elif isinstance(value, int) or isinstance(value, float) \ - or isinstance(value, bool) or isinstance(value, decimal.Decimal): - return str(value) - elif isinstance(value, list) or isinstance(value, dict): - return json.dumps(value) - elif isinstance(value, datetime) or isinstance(value, date) or isinstance(value, time): - return value.isoformat() - elif isinstance(value, Model): - return value.pk - elif isinstance(value, LazyI18nString): - return json.dumps(value.data) - elif isinstance(value, File): - return 'file://' + value.name - - raise TypeError('Unable to serialize %s into a setting.' % str(type(value))) - - def get(self, key: str, default=None, as_type: type=None, binary_file=False): - """ - Get a setting specified by key ``key``. Normally, settings are strings, but - if you put non-strings into the settings object, you can request unserialization - by specifying ``as_type``. If the key does not have a harcdoded type in the pretix source, - omitting ``as_type`` always will get you a string. - - If the setting with the specified name does not exist on this object, any parent object - will be queried (e.g. the organizer of an event). If still no value is found, a default - value hardcoded will be returned if one exists. If not, the value of the ``default`` argument - will be returned instead. - """ - if as_type is None and key in DEFAULTS: - as_type = DEFAULTS[key]['type'] - - if key in self._cache(): - value = self._cache()[key] - else: - value = None - if self._parent: - value = self._parent.settings.get(key, as_type=str) - if value is None and key in DEFAULTS: - value = DEFAULTS[key]['default'] - if value is None and default is not None: - value = default - - return self._unserialize(value, as_type, binary_file=binary_file) - - def __getitem__(self, key: str) -> Any: - return self.get(key) - - def __getattr__(self, key: str) -> Any: - if key.startswith('_'): - return super().__getattr__(key) - return self.get(key) - - def __setattr__(self, key: str, value: Any) -> None: - if key.startswith('_'): - return super().__setattr__(key, value) - self.set(key, value) - - def __setitem__(self, key: str, value: Any) -> None: - self.set(key, value) - - def set(self, key: str, value: Any) -> None: - """ - Stores a setting to the database of its object. - """ - wc = self._write_cache() - if key in wc: - s = wc[key] - else: - s = self._type(object=self._obj, key=key) - s.value = self._serialize(value) - s.save() - self._cache()[key] = s.value - wc[key] = s - self._flush_external_cache() - - def __delattr__(self, key: str) -> None: - if key.startswith('_'): - return super().__delattr__(key) - self.delete(key) - - def __delitem__(self, key: str) -> None: - self.delete(key) - - def delete(self, key: str) -> None: - """ - Deletes a setting from this object's storage. - """ - if key in self._write_cache(): - self._write_cache()[key].delete() - del self._write_cache()[key] - - if key in self._cache(): - del self._cache()[key] - - self._flush_external_cache() +@settings_hierarkey.set_global(cache_namespace='global') +class GlobalSettingsObject(GlobalSettingsBase): + slug = '_global' class SettingsSandbox: @@ -614,13 +442,3 @@ class SettingsSandbox: def set(self, key: str, value: Any): self._event.settings.set(self._convert_key(key), value) - - -class GlobalSettingsObject(): - settings_namespace = 'global' - - def __init__(self): - self.settings = SettingsProxy(self, type=GlobalSetting) - self.setting_objects = GlobalSetting.objects - self.slug = '_global' - self.pk = '_global' diff --git a/src/pretix/control/views/main.py b/src/pretix/control/views/main.py index f2f8d36932..2bd1e043f6 100644 --- a/src/pretix/control/views/main.py +++ b/src/pretix/control/views/main.py @@ -22,13 +22,13 @@ class EventList(ListView): def get_queryset(self): if self.request.user.is_superuser: return Event.objects.all().select_related("organizer").prefetch_related( - "setting_objects", "organizer__setting_objects" + "_settings_objects", "organizer___settings_objects" ) else: return Event.objects.filter( permitted__id__exact=self.request.user.pk ).select_related("organizer").prefetch_related( - "setting_objects", "organizer__setting_objects" + "_settings_objects", "organizer___settings_objects" ) diff --git a/src/requirements/dev.txt b/src/requirements/dev.txt index 83edac0f12..fc6a8b8115 100644 --- a/src/requirements/dev.txt +++ b/src/requirements/dev.txt @@ -7,10 +7,10 @@ pep8-naming flake8 codecov coverage -pytest==2.9.* +pytest==3.0.* pytest-django isort -pytest-mock +pytest-mock==1.4.* pytest-rerunfailures pytest-warnings responses diff --git a/src/requirements/production.txt b/src/requirements/production.txt index b8836d43a2..be616b16aa 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -5,6 +5,7 @@ pytz django-bootstrap3==8.0.* django-formset-js-improved==0.5.0.1 django-compressor==2.1 +django-hierarkey==1.0.* reportlab==3.2.* PyPDF2==1.26.* easy-thumbnails==2.* diff --git a/src/setup.py b/src/setup.py index 5685c0354e..63af8e9f22 100644 --- a/src/setup.py +++ b/src/setup.py @@ -68,6 +68,7 @@ setup( 'django-bootstrap3==7.1.*', 'django-formset-js-improved==0.5.0.1', 'django-compressor==2.1', + 'django-hierarkey==1.0.*', 'reportlab==3.2.*', 'easy-thumbnails==2.*', 'PyPDF2==1.26.*', diff --git a/src/tests/base/test_settings.py b/src/tests/base/test_settings.py index 65f89525c8..2a01b429bf 100644 --- a/src/tests/base/test_settings.py +++ b/src/tests/base/test_settings.py @@ -1,15 +1,9 @@ -from datetime import date, datetime, time -from decimal import Decimal - -from django.core.files import File -from django.core.files.storage import default_storage -from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase from django.utils.timezone import now from i18nfield.strings import LazyI18nString from pretix.base import settings -from pretix.base.models import Event, Organizer, User +from pretix.base.models import Event, Organizer from pretix.base.settings import SettingsSandbox from pretix.control.forms.global_settings import GlobalSettingsObject @@ -21,243 +15,24 @@ class SettingsTestCase(TestCase): 'type': str } self.global_settings = GlobalSettingsObject() - self.global_settings.settings._flush() + self.global_settings.settings.flush() self.organizer = Organizer.objects.create(name='Dummy', slug='dummy') - self.organizer.settings._flush() + self.organizer.settings.flush() self.event = Event.objects.create( organizer=self.organizer, name='Dummy', slug='dummy', date_from=now(), ) - self.event.settings._flush() - - def test_global_set_explicit(self): - self.global_settings.settings.test = 'foo' - self.assertEqual(self.global_settings.settings.test, 'foo') - - # Reload object - self.global_settings = GlobalSettingsObject() - self.assertEqual(self.global_settings.settings.test, 'foo') - - def test_organizer_set_explicit(self): - self.organizer.settings.test = 'foo' - self.assertEqual(self.organizer.settings.test, 'foo') - - # Reload object - self.organizer = Organizer.objects.get(id=self.organizer.id) - self.assertEqual(self.organizer.settings.test, 'foo') - - def test_event_set_explicit(self): - self.event.settings.test = 'foo' - self.assertEqual(self.event.settings.test, 'foo') - - # Reload object - self.event = Event.objects.get(id=self.event.id) - self.assertEqual(self.event.settings.test, 'foo') - - def test_event_set_twice(self): - self.event.settings.test = 'bar' - self.event.settings.test = 'foo' - self.assertEqual(self.event.settings.test, 'foo') - - # Reload object - self.event = Event.objects.get(id=self.event.id) - self.assertEqual(self.event.settings.test, 'foo') - - def test_organizer_set_on_global(self): - self.global_settings.settings.test = 'foo' - self.assertEqual(self.global_settings.settings.test, 'foo') - self.assertEqual(self.organizer.settings.test, 'foo') - - # Reload object - self.global_settings = GlobalSettingsObject() - self.assertEqual(self.global_settings.settings.test, 'foo') - self.assertEqual(self.organizer.settings.test, 'foo') - - def test_event_set_on_global(self): - self.global_settings.settings.test = 'foo' - self.assertEqual(self.global_settings.settings.test, 'foo') - self.assertEqual(self.event.settings.test, 'foo') - - # Reload object - self.global_settings = GlobalSettingsObject() - self.assertEqual(self.global_settings.settings.test, 'foo') - self.assertEqual(self.event.settings.test, 'foo') - - def test_event_set_on_organizer(self): - self.organizer.settings.test = 'foo' - self.assertEqual(self.organizer.settings.test, 'foo') - self.assertEqual(self.event.settings.test, 'foo') - - # Reload object - self.organizer = Organizer.objects.get(id=self.organizer.id) - self.assertEqual(self.organizer.settings.test, 'foo') - self.assertEqual(self.event.settings.test, 'foo') - - def test_event_override_organizer(self): - self.organizer.settings.test = 'foo' - self.event.settings.test = 'bar' - self.assertEqual(self.organizer.settings.test, 'foo') - self.assertEqual(self.event.settings.test, 'bar') - - # Reload object - self.organizer = Organizer.objects.get(id=self.organizer.id) - self.event = Event.objects.get(id=self.event.id) - self.assertEqual(self.organizer.settings.test, 'foo') - self.assertEqual(self.event.settings.test, 'bar') - - def test_event_override_global(self): - self.global_settings.settings.test = 'foo' - self.event.settings.test = 'bar' - self.assertEqual(self.global_settings.settings.test, 'foo') - self.assertEqual(self.event.settings.test, 'bar') - - # Reload object - self.global_settings = GlobalSettingsObject() - self.event = Event.objects.get(id=self.event.id) - self.assertEqual(self.global_settings.settings.test, 'foo') - self.assertEqual(self.event.settings.test, 'bar') - - def test_default(self): - self.assertEqual(self.global_settings.settings.test_default, 'def') - self.assertEqual(self.organizer.settings.test_default, 'def') - self.assertEqual(self.event.settings.test_default, 'def') - self.assertEqual(self.event.settings.get('nonexistant', default='abc'), 'abc') - - def test_default_typing(self): - self.assertIs(type(self.event.settings.get('nonexistant', as_type=Decimal, default=0)), Decimal) - - def test_item_access(self): - self.event.settings['foo'] = 'abc' - self.assertEqual(self.event.settings['foo'], 'abc') - del self.event.settings['foo'] - self.assertIsNone(self.event.settings['foo']) - - def test_delete(self): - self.organizer.settings.test = 'foo' - self.event.settings.test = 'bar' - self.assertEqual(self.organizer.settings.test, 'foo') - self.assertEqual(self.event.settings.test, 'bar') - - del self.event.settings.test - self.assertEqual(self.event.settings.test, 'foo') - - self.event = Event.objects.get(id=self.event.id) - self.assertEqual(self.event.settings.test, 'foo') - - del self.organizer.settings.test - self.assertIsNone(self.organizer.settings.test) - - self.organizer = Organizer.objects.get(id=self.organizer.id) - self.assertIsNone(self.organizer.settings.test) - - def test_serialize_str(self): - self._test_serialization('ABC', as_type=str) - - def test_serialize_float(self): - self._test_serialization(2.3, float) - - def test_serialize_int(self): - self._test_serialization(2, int) - - def test_serialize_datetime(self): - self._test_serialization(now(), datetime) - - def test_serialize_time(self): - self._test_serialization(now().time(), time) - - def test_serialize_date(self): - self._test_serialization(now().date(), date) - - def test_serialize_decimal(self): - self._test_serialization(Decimal('2.3'), Decimal) - - def test_serialize_dict(self): - self._test_serialization({'a': 'b', 'c': 'd'}, dict) - - def test_serialize_list(self): - self._test_serialization([1, 2, 'a'], list) - - def test_serialize_lazyi18nstring(self): - self._test_serialization(LazyI18nString({'de': 'Hallo', 'en': 'Hello'}), LazyI18nString) - - def test_serialize_bool(self): - self._test_serialization(True, bool) - self._test_serialization(False, bool) - - def test_serialize_bool_implicit(self): - self.event.settings.set('test', True) - self.event.settings._flush() - self.assertIs(self.event.settings.get('test', as_type=None), True) - self.event.settings.set('test', False) - self.event.settings._flush() - self.assertIs(self.event.settings.get('test', as_type=None), False) - - def test_serialize_versionable(self): - self._test_serialization(self.event, Event) - - def test_serialize_model(self): - self._test_serialization(User.objects.create_user('dummy@dummy.dummy', 'dummy'), User) - - def test_serialize_unknown(self): - class Type: - pass - - try: - self._test_serialization(Type(), Type) - self.assertTrue(False, 'No exception thrown!') - except TypeError: - pass - - def test_serialize_file(self): - val = SimpleUploadedFile("sample_invalid_image.jpg", b"file_content", content_type="image/jpeg") - default_storage.save(val.name, val) - val.close() - self.event.settings.set('test', val) - self.event.settings._flush() - f = self.event.settings.get('test', as_type=File) - self.assertIsInstance(f, File) - self.assertTrue(f.name.endswith(val.name)) - f.close() - - def test_unserialize_file_value(self): - val = SimpleUploadedFile("sample_invalid_image.jpg", b"file_content", content_type="image/jpeg") - default_storage.save(val.name, val) - val.close() - self.event.settings.set('test', val) - self.event.settings._flush() - f = self.event.settings.get('test', as_type=File) - self.assertIsInstance(f, File) - self.assertTrue(f.name.endswith(val.name)) - f.close() - - def test_autodetect_file_value(self): - val = SimpleUploadedFile("sample_invalid_image.jpg", b"file_content", content_type="image/jpeg") - default_storage.save(val.name, val) - val.close() - self.event.settings.set('test', val) - self.event.settings._flush() - f = self.event.settings.get('test') - self.assertIsInstance(f, File) - self.assertTrue(f.name.endswith(val.name)) - f.close() - - def test_autodetect_file_value_of_parent(self): - val = SimpleUploadedFile("sample_invalid_image.jpg", b"file_content", content_type="image/jpeg") - default_storage.save(val.name, val) - val.close() - self.organizer.settings.set('test', val) - self.organizer.settings._flush() - f = self.event.settings.get('test') - self.assertIsInstance(f, File) - self.assertTrue(f.name.endswith(val.name)) - f.close() + self.event.settings.flush() def _test_serialization(self, val, as_type): self.event.settings.set('test', val) - self.event.settings._flush() + self.event.settings.flush() self.assertEqual(self.event.settings.get('test', as_type=as_type), val) self.assertIsInstance(self.event.settings.get('test', as_type=as_type), as_type) + def test_serialize_lazyi18nstring(self): + self._test_serialization(LazyI18nString({'de': 'Hallo', 'en': 'Hello'}), LazyI18nString) + def test_sandbox(self): sandbox = SettingsSandbox('testing', 'foo', self.event) sandbox.set('foo', 'bar') @@ -278,26 +53,3 @@ class SettingsTestCase(TestCase): self.assertIsNone(sandbox.bar) self.assertIsNone(sandbox['baz']) - - def test_freeze(self): - olddef = settings.DEFAULTS - settings.DEFAULTS = { - 'test_default': { - 'default': 'def', - 'type': str - } - } - self.event.organizer.settings.set('bar', 'baz') - self.event.organizer.settings.set('foo', 'baz') - self.event.settings.set('foo', 'bar') - frozen = self.event.settings.freeze() - self.event.settings.set('foo', 'notfrozen') - - try: - self.assertEqual(frozen, { - 'test_default': 'def', - 'bar': 'baz', - 'foo': 'bar' - }) - finally: - settings.DEFAULTS = olddef diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index 4c41614a26..eb76fa4498 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -135,7 +135,7 @@ class EventsTest(SoupTest): 'settings-payment_term_days': '2', 'settings-tax_rate_default': '19.00', }) - self.event1.settings._flush() + self.event1.settings.flush() assert self.event1.settings.get('payment_banktransfer__enabled', as_type=bool) assert self.event1.settings.get('payment_banktransfer__fee_abs', as_type=Decimal) == Decimal('12.23') @@ -177,7 +177,7 @@ class EventsTest(SoupTest): doc = self.post_doc('/control/event/%s/%s/settings/invoice' % (self.orga1.slug, self.event1.slug), data, follow=True) assert doc.select('.alert-success') - self.event1.settings._flush() + self.event1.settings.flush() assert self.event1.settings.get('invoice_address_required', as_type=bool) def test_display_settings(self): @@ -190,7 +190,7 @@ class EventsTest(SoupTest): doc = self.post_doc('/control/event/%s/%s/settings/display' % (self.orga1.slug, self.event1.slug), data, follow=True) assert doc.select('.alert-success') - self.event1.settings._flush() + self.event1.settings.flush() assert self.event1.settings.get('primary_color') == '#FF0000' mocked.assert_any_call(args=(self.event1.pk,)) @@ -204,7 +204,7 @@ class EventsTest(SoupTest): doc = self.post_doc('/control/event/%s/%s/settings/email' % (self.orga1.slug, self.event1.slug), data, follow=True) assert doc.select('.alert-success') - self.event1.settings._flush() + self.event1.settings.flush() assert mocked.called def test_ticket_settings(self): @@ -214,7 +214,7 @@ class EventsTest(SoupTest): data['ticketoutput_testdummy__enabled'] = 'on' doc = self.post_doc('/control/event/%s/%s/settings/tickets' % (self.orga1.slug, self.event1.slug), data, follow=True) - self.event1.settings._flush() + self.event1.settings.flush() assert self.event1.settings.get('ticket_download', as_type=bool) def test_create_event_unauthorized(self): diff --git a/src/tests/control/test_updatecheck.py b/src/tests/control/test_updatecheck.py index 535181bcaf..a78badfc25 100644 --- a/src/tests/control/test_updatecheck.py +++ b/src/tests/control/test_updatecheck.py @@ -52,12 +52,12 @@ def test_settings(client, user): client.post('/control/global/update/', {'update_check_email': 'test@example.org', 'update_check_perform': 'on'}) gs = GlobalSettingsObject() - gs.settings._flush() + gs.settings.flush() assert gs.settings.update_check_perform assert gs.settings.update_check_email client.post('/control/global/update/', {'update_check_email': '', 'update_check_perform': ''}) - gs.settings._flush() + gs.settings.flush() assert not gs.settings.update_check_perform assert not gs.settings.update_check_email @@ -78,5 +78,5 @@ def test_trigger(client, user): gs = GlobalSettingsObject() assert not gs.settings.update_check_last client.post('/control/global/update/', {'trigger': 'on'}) - gs.settings._flush() + gs.settings.flush() assert gs.settings.update_check_last diff --git a/src/tests/plugins/test_pretixdroid.py b/src/tests/plugins/test_pretixdroid.py index e0f8714162..2523c65722 100644 --- a/src/tests/plugins/test_pretixdroid.py +++ b/src/tests/plugins/test_pretixdroid.py @@ -44,11 +44,11 @@ def test_flush_key(client, env): env[0].settings.set('pretixdroid_key', 'abcdefg') client.get('/control/event/%s/%s/pretixdroid/' % (env[0].organizer.slug, env[0].slug)) - env[0].settings._flush() + env[0].settings.flush() env[0].settings.get('pretixdroid_key') == 'abcdefg' client.get('/control/event/%s/%s/pretixdroid/?flush_key=1' % (env[0].organizer.slug, env[0].slug)) - env[0].settings._flush() + env[0].settings.flush() env[0].settings.get('pretixdroid_key') != 'abcdefg'