Add event meta-data

This commit is contained in:
Raphael Michel
2017-09-04 19:50:32 +02:00
parent 454ca27c54
commit 88f5af3e77
23 changed files with 470 additions and 27 deletions
+8 -2
View File
@@ -26,8 +26,12 @@ presale_end datetime The date at whi
location multi-lingual string The event location (or ``null``) location multi-lingual string The event location (or ``null``)
has_subevents boolean ``True`` if the event series feature is active for this has_subevents boolean ``True`` if the event series feature is active for this
event event
meta_data dict Values set for organizer-specific meta data parameters.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
.. versionchanged:: 1.7
The ``meta_data`` field has been added.
Endpoints Endpoints
--------- ---------
@@ -69,7 +73,8 @@ Endpoints
"presale_start": null, "presale_start": null,
"presale_end": null, "presale_end": null,
"location": null, "location": null,
"has_subevents": false "has_subevents": false,
"meta_data": {}
} }
] ]
} }
@@ -112,7 +117,8 @@ Endpoints
"presale_start": null, "presale_start": null,
"presale_end": null, "presale_end": null,
"location": null, "location": null,
"has_subevents": false "has_subevents": false,
"meta_data": {}
} }
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
+9 -2
View File
@@ -31,8 +31,13 @@ variation_price_overrides list of objects List of variati
the default price the default price
├ variation integer The internal variation ID ├ variation integer The internal variation ID
└ price money (string) The price or ``null`` for the default price └ 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 Endpoints
--------- ---------
@@ -78,7 +83,8 @@ Endpoints
"price": "12.00" "price": "12.00"
} }
], ],
"variation_price_overrides": [] "variation_price_overrides": [],
"meta_data": {}
} }
] ]
} }
@@ -126,7 +132,8 @@ Endpoints
"price": "12.00" "price": "12.00"
} }
], ],
"variation_price_overrides": [] "variation_price_overrides": [],
"meta_data": {}
} }
:param organizer: The ``slug`` field of the organizer to fetch :param organizer: The ``slug`` field of the organizer to fetch
@@ -32,6 +32,15 @@ Organizers and events
.. autoclass:: pretix.base.models.RequiredAction .. autoclass:: pretix.base.models.RequiredAction
:members: :members:
.. autoclass:: pretix.base.models.EventMetaProperty
:members:
.. autoclass:: pretix.base.models.EventMetaValue
:members:
.. autoclass:: pretix.base.models.SubEventMetaValue
:members:
Items Items
----- -----
+14 -2
View File
@@ -1,4 +1,5 @@
from django_countries.serializers import CountryFieldMixin from django_countries.serializers import CountryFieldMixin
from rest_framework.fields import Field
from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import Event, TaxRule 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 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): class EventSerializer(I18nAwareModelSerializer):
meta_data = MetaDataField(source='*')
class Meta: class Meta:
model = Event model = Event
fields = ('name', 'slug', 'live', 'currency', 'date_from', fields = ('name', 'slug', 'live', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start', 'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'has_subevents') 'presale_end', 'location', 'has_subevents', 'meta_data')
class SubEventItemSerializer(I18nAwareModelSerializer): class SubEventItemSerializer(I18nAwareModelSerializer):
@@ -29,12 +40,13 @@ class SubEventItemVariationSerializer(I18nAwareModelSerializer):
class SubEventSerializer(I18nAwareModelSerializer): class SubEventSerializer(I18nAwareModelSerializer):
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True) item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True)
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True) variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True)
meta_data = MetaDataField(source='*')
class Meta: class Meta:
model = SubEvent model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission', fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'presale_start', 'presale_end', 'location',
'item_price_overrides', 'variation_price_overrides') 'item_price_overrides', 'variation_price_overrides', 'meta_data')
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer): class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
+1 -1
View File
@@ -15,7 +15,7 @@ class EventViewSet(viewsets.ReadOnlyModelViewSet):
lookup_url_kwarg = 'event' lookup_url_kwarg = 'event'
def get_queryset(self): 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): class SubEventFilter(FilterSet):
@@ -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')]),
),
]
+83
View File
@@ -452,6 +452,12 @@ class Event(EventMixin, LoggedModel):
) )
).order_by('date_from', 'name') ).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): class SubEvent(EventMixin, LoggedModel):
""" """
@@ -541,6 +547,12 @@ class SubEvent(EventMixin, LoggedModel):
for si in SubEventItemVariation.objects.filter(subevent=self, price__isnull=False) 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(): def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits) return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
@@ -589,3 +601,74 @@ class RequiredAction(models.Model):
if response: if response:
return response return response
return self.action_type 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')
+17
View File
@@ -10,6 +10,7 @@ from pytz import common_timezones, timezone
from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm
from pretix.base.models import Event, Organizer, TaxRule from pretix.base.models import Event, Organizer, TaxRule
from pretix.base.models.event import EventMetaValue
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.control.forms import ExtFileField, SlugWidget from pretix.control.forms import ExtFileField, SlugWidget
from pretix.multidomain.urlreverse import build_absolute_uri 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): class EventUpdateForm(I18nModelForm):
def clean_slug(self): def clean_slug(self):
return self.instance.slug return self.instance.slug
+8
View File
@@ -71,6 +71,14 @@ class OrganizerUpdateForm(OrganizerForm):
return instance return instance
class EventMetaPropertyForm(forms.ModelForm):
class Meta:
fields = ['name', 'default']
widgets = {
'default': forms.TextInput()
}
class TeamForm(forms.ModelForm): class TeamForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
+18 -1
View File
@@ -3,7 +3,7 @@ from django.utils.functional import cached_property
from i18nfield.forms import I18nInlineFormSet from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm 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 from pretix.base.models.items import SubEventItem
@@ -99,3 +99,20 @@ class QuotaFormSet(I18nInlineFormSet):
) )
self.add_fields(form, None) self.add_fields(form, None)
return form 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
}
@@ -15,6 +15,26 @@
{% bootstrap_field form.date_admission layout="horizontal" %} {% bootstrap_field form.date_admission layout="horizontal" %}
{% bootstrap_field form.currency layout="horizontal" %} {% bootstrap_field form.currency layout="horizontal" %}
{% bootstrap_field form.is_public layout="horizontal" %} {% bootstrap_field form.is_public layout="horizontal" %}
{% if meta_forms %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
<div class="col-md-9">
{% for form in meta_forms %}
<div class="row">
<div class="col-md-4">
<label for="{{ form.value.id_for_label }}">
{{ form.property.name }}
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Display settings" %}</legend> <legend>{% trans "Display settings" %}</legend>
@@ -1,6 +1,7 @@
{% extends "pretixcontrol/base.html" %} {% extends "pretixcontrol/base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load formset_tags %}
{% block title %}{% trans "Organizer" %}{% endblock %} {% block title %}{% trans "Organizer" %}{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "Organizer" %}</h1> <h1>{% trans "Organizer" %}</h1>
@@ -28,6 +29,65 @@
{% bootstrap_form_errors sform %} {% bootstrap_form_errors sform %}
{% bootstrap_field sform.organizer_info_text layout="horizontal" %} {% bootstrap_field sform.organizer_info_text layout="horizontal" %}
</fieldset> </fieldset>
<fieldset>
<legend>{% trans "Event metadata (advanced)" %}</legend>
<p>
{% blocktrans trimmed %}
You can here define a set of metadata properties (i.e. variables) that you can later set for your
events and re-use in places like ticket layouts. This is an useful timesaver if you create lots and
lots of events.
{% endblocktrans %}
</p>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
<div class="row" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-5">
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-5 col-lg-6">
{% bootstrap_field form.default layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 col-lg-1 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-5">
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-5 col-lg-6">
{% bootstrap_field formset.empty_form.default layout='inline' form_group_class="" %}
</div>
<div class="col-md-2 col-lg-1 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add property" %}</button>
</p>
</div>
</fieldset>
<div class="form-group submit-group"> <div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %} {% trans "Save" %}
@@ -27,6 +27,25 @@
{% bootstrap_field form.location layout="horizontal" %} {% bootstrap_field form.location layout="horizontal" %}
{% bootstrap_field form.date_admission layout="horizontal" %} {% bootstrap_field form.date_admission layout="horizontal" %}
{% bootstrap_field form.frontpage_text layout="horizontal" %} {% bootstrap_field form.frontpage_text layout="horizontal" %}
{% if meta_forms %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
<div class="col-md-9">
{% for form in meta_forms %}
<div class="row">
<div class="col-md-4">
<label for="{{ form.value.id_for_label }}">
{{ form.property.name }}
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Timeline" %}</legend> <legend>{% trans "Timeline" %}</legend>
+41 -5
View File
@@ -28,13 +28,14 @@ from pretix.base.models import (
CachedTicket, Event, Item, ItemVariation, LogEntry, Order, OrderPosition, CachedTicket, Event, Item, ItemVariation, LogEntry, Order, OrderPosition,
RequiredAction, TaxRule, Voucher, RequiredAction, TaxRule, Voucher,
) )
from pretix.base.models.event import EventMetaValue
from pretix.base.services import tickets from pretix.base.services import tickets
from pretix.base.services.invoices import build_preview_invoice_pdf from pretix.base.services.invoices import build_preview_invoice_pdf
from pretix.base.signals import event_live_issues, register_ticket_outputs from pretix.base.signals import event_live_issues, register_ticket_outputs
from pretix.control.forms.event import ( from pretix.control.forms.event import (
CommentForm, DisplaySettingsForm, EventSettingsForm, EventUpdateForm, CommentForm, DisplaySettingsForm, EventMetaValueForm, EventSettingsForm,
InvoiceSettingsForm, MailSettingsForm, PaymentSettingsForm, ProviderForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm,
TaxRuleForm, TicketSettingsForm, PaymentSettingsForm, ProviderForm, TaxRuleForm, TicketSettingsForm,
) )
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import build_absolute_uri
@@ -44,7 +45,39 @@ from . import CreateView, UpdateView
from ..logdisplay import OVERVIEW_BLACKLIST from ..logdisplay import OVERVIEW_BLACKLIST
class EventUpdate(EventPermissionRequiredMixin, UpdateView): class MetaDataEditorMixin:
meta_form = EventMetaValueForm
meta_model = EventMetaValue
@cached_property
def meta_forms(self):
val_instances = {
v.property_id: v for v in self.object.meta_values.all()
}
formlist = []
for p in self.request.organizer.meta_properties.all():
formlist.append(self._make_meta_form(p, val_instances))
return formlist
def _make_meta_form(self, p, val_instances):
return self.meta_form(
prefix='prop-{}'.format(p.pk),
property=p,
instance=val_instances.get(p.pk, self.meta_model(property=p, event=self.object)),
data=(self.request.POST if self.request.method == "POST" else None)
)
def save_meta(self):
for f in self.meta_forms:
if f.cleaned_data.get('value'):
f.save()
elif f.instance and f.instance.pk:
f.delete()
class EventUpdate(EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
model = Event model = Event
form_class = EventUpdateForm form_class = EventUpdateForm
template_name = 'pretixcontrol/event/settings.html' template_name = 'pretixcontrol/event/settings.html'
@@ -68,11 +101,14 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
def get_context_data(self, *args, **kwargs) -> dict: def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(*args, **kwargs)
context['sform'] = self.sform context['sform'] = self.sform
context['meta_forms'] = self.meta_forms
return context return context
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
self.sform.save() self.sform.save()
self.save_meta()
if self.sform.has_changed(): if self.sform.has_changed():
self.request.event.log_action('pretix.event.settings', user=self.request.user, data={ self.request.event.log_action('pretix.event.settings', user=self.request.user, data={
k: self.request.event.settings.get(k) for k in self.sform.changed_data k: self.request.event.settings.get(k) for k in self.sform.changed_data
@@ -92,7 +128,7 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form = self.get_form() form = self.get_form()
if form.is_valid() and self.sform.is_valid(): if form.is_valid() and self.sform.is_valid() and all([f.is_valid() for f in self.meta_forms]):
# reset timezone # reset timezone
zone = timezone(self.sform.cleaned_data['timezone']) zone = timezone(self.sform.cleaned_data['timezone'])
event = form.instance event = form.instance
+35 -2
View File
@@ -5,6 +5,7 @@ from django.core.files import File
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
from django.db.models import Count from django.db.models import Count
from django.forms import inlineformset_factory
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -13,10 +14,12 @@ from django.views.generic import (
) )
from pretix.base.models import Organizer, Team, TeamInvite, User from pretix.base.models import Organizer, Team, TeamInvite, User
from pretix.base.models.event import EventMetaProperty
from pretix.base.models.organizer import TeamAPIToken from pretix.base.models.organizer import TeamAPIToken
from pretix.base.services.mail import SendMailException, mail from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.organizer import ( from pretix.control.forms.organizer import (
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, EventMetaPropertyForm, OrganizerForm, OrganizerSettingsForm,
OrganizerUpdateForm, TeamForm,
) )
from pretix.control.permissions import OrganizerPermissionRequiredMixin from pretix.control.permissions import OrganizerPermissionRequiredMixin
from pretix.control.signals import nav_organizer from pretix.control.signals import nav_organizer
@@ -108,10 +111,12 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
def get_context_data(self, *args, **kwargs) -> dict: def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs) context = super().get_context_data(*args, **kwargs)
context['sform'] = self.sform context['sform'] = self.sform
context['formset'] = self.formset
return context return context
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
self.save_formset(self.object)
self.sform.save() self.sform.save()
if self.sform.has_changed(): if self.sform.has_changed():
self.request.organizer.log_action( self.request.organizer.log_action(
@@ -145,12 +150,40 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
}) })
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form() form = self.get_form()
if form.is_valid() and self.sform.is_valid(): if form.is_valid() and self.sform.is_valid() and self.formset.is_valid():
return self.form_valid(form) return self.form_valid(form)
else: else:
return self.form_invalid(form) return self.form_invalid(form)
@cached_property
def formset(self):
formsetclass = inlineformset_factory(
Organizer, EventMetaProperty,
form=EventMetaPropertyForm, can_order=False, can_delete=True, extra=0
)
return formsetclass(self.request.POST if self.request.method == "POST" else None,
instance=self.object, queryset=self.object.meta_properties.all())
def save_formset(self, obj):
for form in self.formset.initial_forms:
if form in self.formset.deleted_forms:
if not form.instance.pk:
continue
form.instance.delete()
form.instance.pk = None
elif form.has_changed():
form.save()
for form in self.formset.extra_forms:
if not form.has_changed():
continue
if self.formset._should_delete_form(form):
continue
form.instance.organizer = obj
form.save()
class OrganizerCreate(CreateView): class OrganizerCreate(CreateView):
model = Organizer model = Organizer
+26 -4
View File
@@ -9,14 +9,16 @@ from django.utils.functional import cached_property
from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django.views.generic import CreateView, DeleteView, ListView, UpdateView from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from pretix.base.models.event import SubEvent from pretix.base.models.event import SubEvent, SubEventMetaValue
from pretix.base.models.items import Quota, SubEventItem, SubEventItemVariation from pretix.base.models.items import Quota, SubEventItem, SubEventItemVariation
from pretix.control.forms.filter import SubEventFilterForm from pretix.control.forms.filter import SubEventFilterForm
from pretix.control.forms.item import QuotaForm from pretix.control.forms.item import QuotaForm
from pretix.control.forms.subevents import ( from pretix.control.forms.subevents import (
QuotaFormSet, SubEventForm, SubEventItemForm, SubEventItemVariationForm, QuotaFormSet, SubEventForm, SubEventItemForm, SubEventItemVariationForm,
SubEventMetaValueForm,
) )
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views.event import MetaDataEditorMixin
class SubEventList(EventPermissionRequiredMixin, ListView): class SubEventList(EventPermissionRequiredMixin, ListView):
@@ -85,7 +87,22 @@ class SubEventDelete(EventPermissionRequiredMixin, DeleteView):
}) })
class SubEventEditorMixin: class SubEventEditorMixin(MetaDataEditorMixin):
meta_form = SubEventMetaValueForm
meta_model = SubEventMetaValue
def _make_meta_form(self, p, val_instances):
if not hasattr(self, '_default_meta'):
self._default_meta = self.request.event.meta_data
return self.meta_form(
prefix='prop-{}'.format(p.pk),
property=p,
default=self._default_meta.get(p.name, ''),
instance=val_instances.get(p.pk, self.meta_model(property=p, subevent=self.object)),
data=(self.request.POST if self.request.method == "POST" else None)
)
@cached_property @cached_property
def formset(self): def formset(self):
extra = 0 extra = 0
@@ -159,6 +176,7 @@ class SubEventEditorMixin:
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
ctx['formset'] = self.formset ctx['formset'] = self.formset
ctx['itemvar_forms'] = self.itemvar_forms ctx['itemvar_forms'] = self.itemvar_forms
ctx['meta_forms'] = self.meta_forms
return ctx return ctx
@cached_property @cached_property
@@ -210,7 +228,9 @@ class SubEventEditorMixin:
return formlist return formlist
def is_valid(self, form): def is_valid(self, form):
return form.is_valid() and all([f.is_valid() for f in self.itemvar_forms]) and self.formset.is_valid() return form.is_valid() and all([f.is_valid() for f in self.itemvar_forms]) and self.formset.is_valid() and (
all([f.is_valid() for f in self.meta_forms])
)
class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView): class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView):
@@ -239,6 +259,7 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
self.save_formset(self.object) self.save_formset(self.object)
self.save_meta()
for f in self.itemvar_forms: for f in self.itemvar_forms:
f.save() f.save()
@@ -308,5 +329,6 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
for f in self.itemvar_forms: for f in self.itemvar_forms:
f.instance.subevent = form.instance f.instance.subevent = form.instance
f.save() f.save()
self.object = form.instance
self.save_meta()
return ret return ret
@@ -137,7 +137,7 @@ var editor = {
if (d.content === "other") { if (d.content === "other") {
o.setText(d.text); o.setText(d.text);
} else { } else {
o.setText(editor.text_samples[d.content]); o.setText(editor._get_text_sample(d.content));
} }
} }
@@ -182,6 +182,13 @@ var editor = {
"addons": gettext("Addon 1\nAddon 2"), "addons": gettext("Addon 1\nAddon 2"),
}, },
_get_text_sample: function (key) {
if (key.startsWith('meta:')) {
return key.substr(5);
}
return editor.text_samples[key];
},
_load_pdf: function (dump) { _load_pdf: function (dump) {
// TODO: Loading indicators // TODO: Loading indicators
var url = editor.pdf_url; var url = editor.pdf_url;
@@ -362,7 +369,7 @@ var editor = {
if ($("#toolbox-content").val() === "other") { if ($("#toolbox-content").val() === "other") {
o.setText($("#toolbox-content-other").val()); o.setText($("#toolbox-content-other").val());
} else { } else {
o.setText(editor.text_samples[$("#toolbox-content").val()]); o.setText(editor._get_text_sample($("#toolbox-content").val()));
} }
} }
@@ -304,6 +304,11 @@
<option value="addons">{% trans "List of Add-Ons" %}</option> <option value="addons">{% trans "List of Add-Ons" %}</option>
<option value="organizer">{% trans "Organizer name" %}</option> <option value="organizer">{% trans "Organizer name" %}</option>
<option value="organizer_info_text">{% trans "Organizer info text" %}</option> <option value="organizer_info_text">{% trans "Organizer info text" %}</option>
{% for p in request.organizer.meta_properties.all %}
<option value="meta:{{ p.name }}">
{% trans "Event attribute:" %} {{ p.name }}
</option>
{% endfor %}
<option value="other">{% trans "Other…" %}</option> <option value="other">{% trans "Other…" %}</option>
</select> </select>
<textarea type="text" value="" class="input-block-level form-control" <textarea type="text" value="" class="input-block-level form-control"
@@ -69,6 +69,8 @@ class PdfTicketOutput(BaseTicketOutput):
ev = op.subevent or order.event ev = op.subevent or order.event
if o['content'] == 'other': if o['content'] == 'other':
return o['text'].replace("\n", "<br/>\n") return o['text'].replace("\n", "<br/>\n")
elif o['content'].startswith('meta:'):
return ev.meta_data.get(o['content'][5:])
elif o['content'] == 'order': elif o['content'] == 'order':
return order.code return order.code
elif o['content'] == 'item': elif o['content'] == 'item':
@@ -175,3 +175,11 @@ pre.mail-preview {
} }
} }
.metadata-group {
.form-group {
margin: 0 0 10px 0;
}
.row label {
padding-top: 7px;
}
}
+15 -5
View File
@@ -18,12 +18,19 @@ def organizer():
@pytest.fixture @pytest.fixture
def event(organizer): def meta_prop(organizer):
return Event.objects.create( return organizer.meta_properties.create(name="type", default="Concert")
@pytest.fixture
def event(organizer, meta_prop):
e = Event.objects.create(
organizer=organizer, name='Dummy', slug='dummy', organizer=organizer, name='Dummy', slug='dummy',
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC), date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC),
plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf' plugins='pretix.plugins.banktransfer,pretix.plugins.ticketoutputpdf'
) )
e.meta_values.create(property=meta_prop, value="Conference")
return e
@pytest.fixture @pytest.fixture
@@ -48,11 +55,14 @@ def token_client(client, team):
@pytest.fixture @pytest.fixture
def subevent(event): def subevent(event, meta_prop):
event.has_subevents = True event.has_subevents = True
event.save() event.save()
return event.subevents.create(name="Foobar", se = event.subevents.create(name="Foobar",
date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC)) date_from=datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
se.meta_values.create(property=meta_prop, value="Workshop")
return se
@pytest.fixture @pytest.fixture
+1
View File
@@ -13,6 +13,7 @@ TEST_EVENT_RES = {
"location": None, "location": None,
"slug": "dummy", "slug": "dummy",
"has_subevents": False, "has_subevents": False,
"meta_data": {"type": "Conference"}
} }
+2 -1
View File
@@ -11,7 +11,8 @@ TEST_SUBEVENT_RES = {
'id': 1, 'id': 1,
'variation_price_overrides': [], 'variation_price_overrides': [],
'location': None, 'location': None,
'item_price_overrides': [] 'item_price_overrides': [],
'meta_data': {'type': 'Workshop'}
} }