diff --git a/doc/api/resources/events.rst b/doc/api/resources/events.rst index 684b236966..fd42a7ee32 100644 --- a/doc/api/resources/events.rst +++ b/doc/api/resources/events.rst @@ -26,8 +26,12 @@ presale_end datetime The date at whi location multi-lingual string The event location (or ``null``) has_subevents boolean ``True`` if the event series feature is active for this event +meta_data dict Values set for organizer-specific meta data parameters. ===================================== ========================== ======================================================= +.. versionchanged:: 1.7 + + The ``meta_data`` field has been added. Endpoints --------- @@ -69,7 +73,8 @@ Endpoints "presale_start": null, "presale_end": null, "location": null, - "has_subevents": false + "has_subevents": false, + "meta_data": {} } ] } @@ -112,7 +117,8 @@ Endpoints "presale_start": null, "presale_end": null, "location": null, - "has_subevents": false + "has_subevents": false, + "meta_data": {} } :param organizer: The ``slug`` field of the organizer to fetch diff --git a/doc/api/resources/subevents.rst b/doc/api/resources/subevents.rst index cd40013f1e..6dacabd596 100644 --- a/doc/api/resources/subevents.rst +++ b/doc/api/resources/subevents.rst @@ -31,8 +31,13 @@ variation_price_overrides list of objects List of variati the default price ├ variation integer The internal variation ID └ price money (string) The price or ``null`` for the default price +meta_data dict Values set for organizer-specific meta data parameters. ===================================== ========================== ======================================================= +.. versionchanged:: 1.7 + + The ``meta_data`` field has been added. + Endpoints --------- @@ -78,7 +83,8 @@ Endpoints "price": "12.00" } ], - "variation_price_overrides": [] + "variation_price_overrides": [], + "meta_data": {} } ] } @@ -126,7 +132,8 @@ Endpoints "price": "12.00" } ], - "variation_price_overrides": [] + "variation_price_overrides": [], + "meta_data": {} } :param organizer: The ``slug`` field of the organizer to fetch diff --git a/doc/development/implementation/models.rst b/doc/development/implementation/models.rst index 6cf60c1939..12764a19a2 100644 --- a/doc/development/implementation/models.rst +++ b/doc/development/implementation/models.rst @@ -32,6 +32,15 @@ Organizers and events .. autoclass:: pretix.base.models.RequiredAction :members: +.. autoclass:: pretix.base.models.EventMetaProperty + :members: + +.. autoclass:: pretix.base.models.EventMetaValue + :members: + +.. autoclass:: pretix.base.models.SubEventMetaValue + :members: + Items ----- diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 08c846e492..8225396f80 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -1,4 +1,5 @@ from django_countries.serializers import CountryFieldMixin +from rest_framework.fields import Field from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.base.models import Event, TaxRule @@ -6,12 +7,22 @@ from pretix.base.models.event import SubEvent from pretix.base.models.items import SubEventItem, SubEventItemVariation +class MetaDataField(Field): + + def to_representation(self, value): + return { + v.property.name: v.value for v in value.meta_values.all() + } + + class EventSerializer(I18nAwareModelSerializer): + meta_data = MetaDataField(source='*') + class Meta: model = Event fields = ('name', 'slug', 'live', 'currency', 'date_from', 'date_to', 'date_admission', 'is_public', 'presale_start', - 'presale_end', 'location', 'has_subevents') + 'presale_end', 'location', 'has_subevents', 'meta_data') class SubEventItemSerializer(I18nAwareModelSerializer): @@ -29,12 +40,13 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer): class SubEventSerializer(I18nAwareModelSerializer): item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True) variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True) + meta_data = MetaDataField(source='*') class Meta: model = SubEvent fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission', 'presale_start', 'presale_end', 'location', - 'item_price_overrides', 'variation_price_overrides') + 'item_price_overrides', 'variation_price_overrides', 'meta_data') class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer): diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py index 11a1ac06bf..365e1857af 100644 --- a/src/pretix/api/views/event.py +++ b/src/pretix/api/views/event.py @@ -15,7 +15,7 @@ class EventViewSet(viewsets.ReadOnlyModelViewSet): lookup_url_kwarg = 'event' def get_queryset(self): - return self.request.organizer.events.all() + return self.request.organizer.events.prefetch_related('meta_values', 'meta_values__property') class SubEventFilter(FilterSet): diff --git a/src/pretix/base/migrations/0075_auto_20170828_0901.py b/src/pretix/base/migrations/0075_auto_20170828_0901.py new file mode 100644 index 0000000000..53f2732301 --- /dev/null +++ b/src/pretix/base/migrations/0075_auto_20170828_0901.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-28 09:01 +from __future__ import unicode_literals + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + +import pretix.base.models.base + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0074_auto_20170825_1258'), + ] + + operations = [ + migrations.CreateModel( + name='EventMetaProperty', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, help_text='Can not contain spaces or special characters execpt underscores', max_length=50, validators=[django.core.validators.RegexValidator(message='The property name may only contain letters, numbers and underscores.', regex='^[a-zA-Z0-9_]+$')], verbose_name='Name')), + ('default', models.TextField()), + ('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_properties', to='pretixbase.Organizer')), + ], + options={ + 'abstract': False, + }, + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + migrations.CreateModel( + name='EventMetaValue', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.TextField()), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values', to='pretixbase.Event')), + ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_values', to='pretixbase.EventMetaProperty')), + ], + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + migrations.CreateModel( + name='SubEventMetaValue', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.TextField()), + ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subevent_values', to='pretixbase.EventMetaProperty')), + ('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values', to='pretixbase.SubEvent')), + ], + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + migrations.AlterUniqueTogether( + name='subeventmetavalue', + unique_together=set([('subevent', 'property')]), + ), + migrations.AlterUniqueTogether( + name='eventmetavalue', + unique_together=set([('event', 'property')]), + ), + ] diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 18583ba4d6..5ff910b785 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -452,6 +452,12 @@ class Event(EventMixin, LoggedModel): ) ).order_by('date_from', 'name') + @property + def meta_data(self): + data = {p.name: p.default for p in self.organizer.meta_properties.all()} + data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()}) + return data + class SubEvent(EventMixin, LoggedModel): """ @@ -541,6 +547,12 @@ class SubEvent(EventMixin, LoggedModel): for si in SubEventItemVariation.objects.filter(subevent=self, price__isnull=False) } + @property + def meta_data(self): + data = self.event.meta_data + data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()}) + return data + def generate_invite_token(): return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits) @@ -589,3 +601,74 @@ class RequiredAction(models.Model): if response: return response return self.action_type + + +class EventMetaProperty(LoggedModel): + """ + An organizer account can have EventMetaProperty objects attached to define meta information fields + for its events. This information can be re-used for example in ticket layouts. + + :param organizer: The organizer this property is defined for. + :type organizer: Organizer + :param name: Name + :type name: Name of the property, used in various places + :param default: Default value + :type default: str + """ + organizer = models.ForeignKey(Organizer, related_name="meta_properties", on_delete=models.CASCADE) + name = models.CharField( + max_length=50, db_index=True, + help_text=_( + "Can not contain spaces or special characters execpt underscores" + ), + validators=[ + RegexValidator( + regex="^[a-zA-Z0-9_]+$", + message=_("The property name may only contain letters, numbers and underscores."), + ), + ], + verbose_name=_("Name"), + ) + default = models.TextField(blank=True) + + +class EventMetaValue(LoggedModel): + """ + A meta-data value assigned to an event. + + :param event: The event this metadata is valid for + :type event: Event + :param property: The property this value belongs to + :type property: EventMetaProperty + :param value: The actual value + :type value: str + """ + event = models.ForeignKey('Event', on_delete=models.CASCADE, + related_name='meta_values') + property = models.ForeignKey('EventMetaProperty', on_delete=models.CASCADE, + related_name='event_values') + value = models.TextField() + + class Meta: + unique_together = ('event', 'property') + + +class SubEventMetaValue(LoggedModel): + """ + A meta-data value assigned to a sub-event. + + :param event: The event this metadata is valid for + :type event: Event + :param property: The property this value belongs to + :type property: EventMetaProperty + :param value: The actual value + :type value: str + """ + subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE, + related_name='meta_values') + property = models.ForeignKey('EventMetaProperty', on_delete=models.CASCADE, + related_name='subevent_values') + value = models.TextField() + + class Meta: + unique_together = ('subevent', 'property') diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index bce55e7b05..19a35f4888 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -10,6 +10,7 @@ from pytz import common_timezones, timezone from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm from pretix.base.models import Event, Organizer, TaxRule +from pretix.base.models.event import EventMetaValue from pretix.base.reldate import RelativeDateField, RelativeDateTimeField from pretix.control.forms import ExtFileField, SlugWidget from pretix.multidomain.urlreverse import build_absolute_uri @@ -163,6 +164,22 @@ class EventWizardCopyForm(forms.Form): ) +class EventMetaValueForm(forms.ModelForm): + + def __init__(self, *args, **kwargs): + self.property = kwargs.pop('property') + super().__init__(*args, **kwargs) + self.fields['value'].required = False + self.fields['value'].widget.attrs['placeholder'] = self.property.default + + class Meta: + model = EventMetaValue + fields = ['value'] + widgets = { + 'value': forms.TextInput + } + + class EventUpdateForm(I18nModelForm): def clean_slug(self): return self.instance.slug diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index 016801180e..e7b35948ab 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -71,6 +71,14 @@ class OrganizerUpdateForm(OrganizerForm): return instance +class EventMetaPropertyForm(forms.ModelForm): + class Meta: + fields = ['name', 'default'] + widgets = { + 'default': forms.TextInput() + } + + class TeamForm(forms.ModelForm): def __init__(self, *args, **kwargs): diff --git a/src/pretix/control/forms/subevents.py b/src/pretix/control/forms/subevents.py index 3de1ce7198..d395742288 100644 --- a/src/pretix/control/forms/subevents.py +++ b/src/pretix/control/forms/subevents.py @@ -3,7 +3,7 @@ from django.utils.functional import cached_property from i18nfield.forms import I18nInlineFormSet from pretix.base.forms import I18nModelForm -from pretix.base.models.event import SubEvent +from pretix.base.models.event import SubEvent, SubEventMetaValue from pretix.base.models.items import SubEventItem @@ -99,3 +99,20 @@ class QuotaFormSet(I18nInlineFormSet): ) self.add_fields(form, None) return form + + +class SubEventMetaValueForm(forms.ModelForm): + + def __init__(self, *args, **kwargs): + self.property = kwargs.pop('property') + self.default = kwargs.pop('default', None) + super().__init__(*args, **kwargs) + self.fields['value'].required = False + self.fields['value'].widget.attrs['placeholder'] = self.default or self.property.default + + class Meta: + model = SubEventMetaValue + fields = ['value'] + widgets = { + 'value': forms.TextInput + } diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index d75cb458ec..8f379f566d 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -15,6 +15,26 @@ {% bootstrap_field form.date_admission layout="horizontal" %} {% bootstrap_field form.currency layout="horizontal" %} {% bootstrap_field form.is_public layout="horizontal" %} + + {% if meta_forms %} +
+ {% endif %} +