diff --git a/src/pretix/base/migrations/0009_eventsetting_organizersetting.py b/src/pretix/base/migrations/0009_eventsetting_organizersetting.py new file mode 100644 index 0000000000..a735eb3fd1 --- /dev/null +++ b/src/pretix/base/migrations/0009_eventsetting_organizersetting.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import versions.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0008_quota_locked'), + ] + + operations = [ + migrations.CreateModel( + name='EventSetting', + fields=[ + ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('identity', models.CharField(max_length=36)), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('key', models.CharField(max_length=255)), + ('value', models.TextField()), + ('event', versions.models.VersionedForeignKey(related_name='setting_objects', to='pretixbase.Event')), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='OrganizerSetting', + fields=[ + ('id', models.CharField(primary_key=True, serialize=False, max_length=36)), + ('identity', models.CharField(max_length=36)), + ('version_start_date', models.DateTimeField()), + ('version_end_date', models.DateTimeField(blank=True, default=None, null=True)), + ('version_birth_date', models.DateTimeField()), + ('key', models.CharField(max_length=255)), + ('value', models.TextField()), + ('organizer', versions.models.VersionedForeignKey(related_name='setting_objects', to='pretixbase.Organizer')), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), + ] diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index 84aefbe1d4..e14e659960 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -7,6 +7,7 @@ from django.db import models from django.conf import settings from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin from django.db.models import Q, Count +from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from django.template.defaultfilters import date as _date @@ -227,6 +228,58 @@ class Organizer(Versionable): def __str__(self): return self.name + class OrganizerSettingsProxy: + """ + This objects allows convenient access to settings stored in the + OrganizerSettings database model. It exposes all settings as properties + and it will do all the nasty defaults stuff for + you. It will return None for non-existing properties. + """ + + def __init__(self, organizer): + self._organizer = organizer + self._cached_obj = None + + def _cache(self): + if self._cached_obj is None: + self._cached_obj = {} + for setting in self._organizer.setting_objects.current.all(): + self._cached_obj[setting.key] = setting + return self._cached_obj + + def __getattr__(self, key): + if key in self._cache(): + return self._cache()[key].value + if key in OrganizerSetting.DEFAULTS: + return OrganizerSetting.DEFAULTS[key] + return None + + def __delattr__(self, key): + if key.startswith('_'): + return super().__delattr__(key) + if key in self._cache(): + self._cache()[key].delete() + del self._cache()[key] + + def __setattr__(self, key, value): + if key.startswith('_'): + return super().__setattr__(key, value) + if key in self._cache(): + s = self._cache()[key] + s = s.clone() + else: + s = OrganizerSetting(organizer=self._organizer, key=key) + s.value = value + s.save() + self._cache()[key] = s + + @cached_property + def settings(self): + """ + Returns an object representing this organizer's settings + """ + return Organizer.OrganizerSettingsProxy(self) + class OrganizerPermission(Versionable): """ @@ -386,6 +439,56 @@ class Event(Versionable): from pretix.base.cache import EventRelatedCache return EventRelatedCache(self) + class EventSettingsProxy: + """ + This objects allows convenient access to settings stored in the + EventSettings database model. It exposes all settings as properties + and it will do all the nasty inheritance and defaults stuff for + you. It will return None for non-existing properties. + """ + + def __init__(self, event): + self._event = event + self._cached_obj = None + + def _cache(self): + if self._cached_obj is None: + self._cached_obj = {} + for setting in self._event.setting_objects.current.all(): + self._cached_obj[setting.key] = setting + return self._cached_obj + + def __getattr__(self, key): + if key in self._cache(): + return self._cache()[key].value + return getattr(self._event.organizer.settings, key) + + def __setattr__(self, key, value): + if key.startswith('_'): + return super().__setattr__(key, value) + if key in self._cache(): + s = self._cache()[key] + s = s.clone() + else: + s = EventSetting(event=self._event, key=key) + s.value = value + s.save() + self._cache()[key] = s + + def __delattr__(self, key): + if key.startswith('_'): + return super().__delattr__(key) + if key in self._cache(): + self._cache()[key].delete() + del self._cache()[key] + + @cached_property + def settings(self): + """ + Returns an object representing this event's settings + """ + return Event.EventSettingsProxy(self) + class EventPermission(Versionable): """ @@ -1320,3 +1423,26 @@ class CartPosition(Versionable): class Meta: verbose_name = _("Cart position") verbose_name_plural = _("Cart positions") + + +class EventSetting(Versionable): + """ + An event settings is a key-value setting which can be set for a + specific event + """ + event = VersionedForeignKey(Event, related_name='setting_objects') + key = models.CharField(max_length=255) + value = models.TextField() + + +class OrganizerSetting(Versionable): + """ + An event option is a key-value setting which can be set for an + organizer. It will be inherited by the events of this organizer + """ + DEFAULTS = { + + } + organizer = VersionedForeignKey(Organizer, related_name='setting_objects') + key = models.CharField(max_length=255) + value = models.TextField() diff --git a/src/pretix/base/tests/test_models.py b/src/pretix/base/tests/test_models.py index cf113a6c7c..f8499d7b8d 100644 --- a/src/pretix/base/tests/test_models.py +++ b/src/pretix/base/tests/test_models.py @@ -5,8 +5,8 @@ from django.utils.timezone import now from pretix.base.models import ( Event, Organizer, Item, ItemVariation, Property, PropertyValue, User, Quota, - Order, OrderPosition, CartPosition -) + Order, OrderPosition, CartPosition, + OrganizerSetting) from pretix.base.types import VariationDict @@ -292,3 +292,67 @@ class QuotaTestCase(TestCase): quota2.size = 0 quota2.save() self.assertEqual(self.item1.check_quotas(), (Quota.AVAILABILITY_GONE, 0)) + + +class SettingsTestCase(TestCase): + + def setUp(self): + OrganizerSetting.DEFAULTS['test_default'] = 'def' + self.organizer = Organizer.objects.create(name='Dummy', slug='dummy') + self.event = Event.objects.create( + organizer=self.organizer, name='Dummy', slug='dummy', + date_from=now(), + ) + + 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(identity=self.event.identity) + 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(identity=self.organizer.identity) + self.event = Event.objects.get(identity=self.event.identity) + self.assertEqual(self.organizer.settings.test, 'foo') + self.assertEqual(self.event.settings.test, 'foo') + + def test_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(identity=self.organizer.identity) + self.event = Event.objects.get(identity=self.event.identity) + self.assertEqual(self.organizer.settings.test, 'foo') + self.assertEqual(self.event.settings.test, 'bar') + + def test_default(self): + self.assertEqual(self.organizer.settings.test_default, 'def') + self.assertEqual(self.event.settings.test_default, 'def') + + 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(identity=self.event.identity) + self.assertEqual(self.event.settings.test, 'foo') + + del self.organizer.settings.test + self.assertIsNone(self.organizer.settings.test) + + self.organizer = Organizer.objects.get(identity=self.organizer.identity) + self.assertIsNone(self.organizer.settings.test)