Add more features to custom meta properties (#1922)

This commit is contained in:
Raphael Michel
2021-02-10 11:01:25 +01:00
committed by GitHub
parent a0f60c71b9
commit f0fd4272dc
23 changed files with 462 additions and 150 deletions

View File

@@ -13,7 +13,7 @@ from rest_framework.relations import SlugRelatedField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.models import Event, TaxRule
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
from pretix.base.models.event import SubEvent
from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.services.seating import (
@@ -174,9 +174,12 @@ class EventSerializer(I18nAwareModelSerializer):
}
def validate_meta_data(self, value):
for key in value['meta_data'].keys():
for key, v in value['meta_data'].items():
if key not in self.meta_properties:
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
if self.meta_properties[key].allowed_values:
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
return value
@cached_property
@@ -223,6 +226,14 @@ class EventSerializer(I18nAwareModelSerializer):
return value
@cached_property
def ignored_meta_properties(self):
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
else self.context['request'].user)
if perm_holder.has_organizer_permission('can_change_organizer_settings', request=self.context['request']):
return []
return [k for k, p in self.meta_properties.items() if p.protected]
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
@@ -238,10 +249,11 @@ class EventSerializer(I18nAwareModelSerializer):
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
if key not in self.ignored_meta_properties:
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
# Item Meta properties
if item_meta_properties is not None:
@@ -279,19 +291,21 @@ class EventSerializer(I18nAwareModelSerializer):
if meta_data is not None:
current = {mv.property: mv for mv in event.meta_values.select_related('property')}
for key, value in meta_data.items():
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
if key not in self.ignored_meta_properties:
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
event.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
for prop, current_object in current.items():
if prop.name not in meta_data:
current_object.delete()
if prop.name not in self.ignored_meta_properties:
if prop.name not in meta_data:
current_object.delete()
# Item Meta properties
if item_meta_properties is not None:
@@ -444,11 +458,22 @@ class SubEventSerializer(I18nAwareModelSerializer):
}
def validate_meta_data(self, value):
for key in value['meta_data'].keys():
for key, v in value['meta_data'].items():
if key not in self.meta_properties:
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
if self.meta_properties[key].allowed_values:
if v not in [_v.strip() for _v in self.meta_properties[key].allowed_values.splitlines()]:
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
return value
@cached_property
def ignored_meta_properties(self):
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
else self.context['request'].user)
if perm_holder.has_organizer_permission('can_change_organizer_settings', request=self.context['request']):
return []
return [k for k, p in self.meta_properties.items() if p.protected]
@transaction.atomic
def create(self, validated_data):
item_price_overrides_data = validated_data.pop('subeventitem_set') if 'subeventitem_set' in validated_data else {}
@@ -465,10 +490,11 @@ class SubEventSerializer(I18nAwareModelSerializer):
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
if key not in self.ignored_meta_properties:
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
# Seats
if subevent.seating_plan:
@@ -514,19 +540,21 @@ class SubEventSerializer(I18nAwareModelSerializer):
if meta_data is not None:
current = {mv.property: mv for mv in subevent.meta_values.select_related('property')}
for key, value in meta_data.items():
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
if key not in self.ignored_meta_properties:
prop = self.meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
subevent.meta_values.create(
property=self.meta_properties.get(key),
value=value
)
for prop, current_object in current.items():
if prop.name not in meta_data:
current_object.delete()
if prop.name not in self.ignored_meta_properties:
if prop.name not in meta_data:
current_object.delete()
# Seats
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.0.11 on 2021-02-05 15:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0175_orderrefund_comment'),
]
operations = [
migrations.AddField(
model_name='eventmetaproperty',
name='allowed_values',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='eventmetaproperty',
name='protected',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='eventmetaproperty',
name='required',
field=models.BooleanField(default=False),
),
]

View File

@@ -16,11 +16,12 @@ from django.core.validators import (
from django.db import models
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery, Value
from django.template.defaultfilters import date as _date
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext, gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from i18nfield.fields import I18nCharField, I18nTextField
@@ -953,6 +954,18 @@ class Event(EventMixin, LoggedModel):
if not self.quotas.exists():
issues.append(_('You need to configure at least one quota to sell anything.'))
for mp in self.organizer.meta_properties.all():
if mp.required and not self.meta_data.get(mp.name):
issues.append(
('<a {a_attr}>' + gettext('You need to fill the meta parameter "{property}".') + '</a>').format(
property=mp.name,
a_attr='href="%s#id_prop-%d-value"' % (
reverse('control:event.settings', kwargs={'organizer': self.organizer.slug, 'event': self.slug}),
mp.pk
)
)
)
responses = event_live_issues.send(self)
for receiver, response in sorted(responses, key=lambda r: str(r[0])):
if response:
@@ -1363,7 +1376,26 @@ class EventMetaProperty(LoggedModel):
],
verbose_name=_("Name"),
)
default = models.TextField(blank=True)
default = models.TextField(blank=True, verbose_name=_("Default value"))
protected = models.BooleanField(default=False,
verbose_name=_("Can only be changed by organizer-level administrators"))
required = models.BooleanField(
default=False, verbose_name=_("Required for events"),
help_text=_("If checked, an event can only be taken live if the property is set. In event series, its always "
"optional to set a value for individual dates")
)
allowed_values = models.TextField(
null=True, blank=True,
verbose_name=_("Valid values"),
help_text=_("If you keep this empty, any value is allowed. Otherwise, enter one possible value per line.")
)
def full_clean(self, exclude=None, validate_unique=True):
super().full_clean(exclude, validate_unique)
if self.default and self.required:
raise ValidationError(_("A property can either be required or have a default value, not both."))
if self.default and self.allowed_values and self.default not in self.allowed_values.splitlines():
raise ValidationError(_("You cannot set a default value that is not a valid value."))
class EventMetaValue(LoggedModel):

View File

@@ -265,15 +265,32 @@ class EventMetaValueForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.property = kwargs.pop('property')
self.disabled = kwargs.pop('disabled')
super().__init__(*args, **kwargs)
if self.property.allowed_values:
self.fields['value'] = forms.ChoiceField(
label=self.property.name,
choices=[
('', _('Default ({value})').format(value=self.property.default) if self.property.default else ''),
] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()],
)
else:
self.fields['value'].label = self.property.name
self.fields['value'].widget.attrs['placeholder'] = self.property.default
self.fields['value'].widget.attrs['data-typeahead-url'] = (
reverse('control:events.meta.typeahead') + '?' + urlencode({
'property': self.property.name,
'organizer': self.property.organizer.slug,
})
)
self.fields['value'].required = False
self.fields['value'].widget.attrs['placeholder'] = self.property.default
self.fields['value'].widget.attrs['data-typeahead-url'] = (
reverse('control:events.meta.typeahead') + '?' + urlencode({
'property': self.property.name,
'organizer': self.property.organizer.slug,
})
)
if self.disabled:
self.fields['value'].widget.attrs['readonly'] = 'readonly'
def clean_slug(self):
if self.disabled:
return self.instance.value if self.instance else None
return self.cleaned_data['slug']
class Meta:
model = EventMetaValue

View File

@@ -13,7 +13,9 @@ from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.forms.widgets import SplitDateTimePickerWidget
from pretix.base.models import Device, Gate, GiftCard, Organizer, Team
from pretix.base.models import (
Device, EventMetaProperty, Gate, GiftCard, Organizer, Team,
)
from pretix.control.forms import ExtFileField, SplitDateTimeField
from pretix.control.forms.event import SafeEventMultipleChoiceField
from pretix.multidomain.models import KnownDomain
@@ -125,7 +127,8 @@ class OrganizerUpdateForm(OrganizerForm):
class EventMetaPropertyForm(forms.ModelForm):
class Meta:
fields = ['name', 'default']
model = EventMetaProperty
fields = ['name', 'default', 'required', 'protected', 'allowed_values']
widgets = {
'default': forms.TextInput()
}

View File

@@ -162,15 +162,32 @@ class SubEventMetaValueForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.property = kwargs.pop('property')
self.default = kwargs.pop('default', None)
self.disabled = kwargs.pop('disabled')
super().__init__(*args, **kwargs)
if self.property.allowed_values:
self.fields['value'] = forms.ChoiceField(
label=self.property.name,
choices=[
('', _('Default ({value})').format(value=self.default or self.property.default) if self.default or self.property.default else ''),
] + [(a.strip(), a.strip()) for a in self.property.allowed_values.splitlines()],
)
else:
self.fields['value'].label = self.property.name
self.fields['value'].widget.attrs['placeholder'] = self.default or self.property.default
self.fields['value'].widget.attrs['data-typeahead-url'] = (
reverse('control:events.meta.typeahead') + '?' + urlencode({
'property': self.property.name,
'organizer': self.property.organizer.slug,
})
)
self.fields['value'].required = False
self.fields['value'].widget.attrs['placeholder'] = self.default or self.property.default
self.fields['value'].widget.attrs['data-typeahead-url'] = (
reverse('control:events.meta.typeahead') + '?' + urlencode({
'property': self.property.name,
'organizer': self.property.organizer.slug,
})
)
if self.disabled:
self.fields['value'].widget.attrs['readonly'] = 'readonly'
def clean_slug(self):
if self.disabled:
return self.instance.value if self.instance else None
return self.cleaned_data['slug']
class Meta:
model = SubEventMetaValue

View File

@@ -427,6 +427,13 @@ def get_organizer_navigation(request):
}),
'active': url.url_name == 'organizer.edit',
},
{
'label': _('Event metadata'),
'url': reverse('control:organizer.properties', kwargs={
'organizer': request.organizer.slug
}),
'active': url.url_name.startswith('organizer.propert'),
},
]
})
if 'can_change_teams' in request.orgapermset:

View File

@@ -70,7 +70,7 @@
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" %}
{% bootstrap_form form layout="inline" error_types="all" %}
</div>
</div>
{% endfor %}

View File

@@ -70,65 +70,6 @@
{% bootstrap_field sform.giftcard_expiry_years layout="control" %}
{% bootstrap_field sform.giftcard_length layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Event metadata" %}</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 flip">
<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 flip">
<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>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -8,7 +8,7 @@
<p>{% blocktrans %}Are you sure you want to delete the gate?{% endblocktrans %}
</p>
<div class="form-group submit-group">
<a href="{% url "control:organizer.teams" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
<a href="{% url "control:organizer.gates" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">

View File

@@ -0,0 +1,42 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h1>{% trans "Event metadata" %}</h1>
<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>
<a href="{% url "control:organizer.property.add" organizer=request.organizer.slug %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new property" %}
</a>
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Property" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for p in properties %}
<tr>
<td><strong>
<a href="{% url "control:organizer.property.edit" organizer=request.organizer.slug property=p.id %}">
{{ p.name }}
</a>
</strong></td>
<td class="text-right flip">
<a href="{% url "control:organizer.property.edit" organizer=request.organizer.slug property=p.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:organizer.property.delete" organizer=request.organizer.slug property=p.id %}"
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
<h1>{% trans "Delete property:" %} {{ gate.name }}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans %}Are you sure you want to delete the property?{% endblocktrans %}
</p>
<div class="form-group submit-group">
<a href="{% url "control:organizer.properties" organizer=request.organizer.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inner %}
{% if gate %}
<h1>{% trans "Property:" %} {{ property.name }}</h1>
{% else %}
<h1>{% trans "Create a new property" %}</h1>
{% endif %}
<form class="form-horizontal" action="" method="post">
{% csrf_token %}
{% bootstrap_form form layout="control" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -422,7 +422,7 @@
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" %}
{% bootstrap_form form layout="inline" error_types="all" %}
</div>
</div>
{% endfor %}

View File

@@ -64,7 +64,7 @@
</label>
</div>
<div class="col-md-8">
{% bootstrap_form form layout="inline" %}
{% bootstrap_form form layout="inline" error_types="all" %}
</div>
</div>
{% endfor %}

View File

@@ -77,6 +77,13 @@ urlpatterns = [
url(r'^organizer/(?P<organizer>[^/]+)/delete$', organizer.OrganizerDelete.as_view(), name='organizer.delete'),
url(r'^organizer/(?P<organizer>[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(),
name='organizer.display'),
url(r'^organizer/(?P<organizer>[^/]+)/properties$', organizer.EventMetaPropertyListView.as_view(), name='organizer.properties'),
url(r'^organizer/(?P<organizer>[^/]+)/property/add$', organizer.EventMetaPropertyCreateView.as_view(),
name='organizer.property.add'),
url(r'^organizer/(?P<organizer>[^/]+)/property/(?P<property>[^/]+)/edit$', organizer.EventMetaPropertyUpdateView.as_view(),
name='organizer.property.edit'),
url(r'^organizer/(?P<organizer>[^/]+)/property/(?P<property>[^/]+)/delete$', organizer.EventMetaPropertyDeleteView.as_view(),
name='organizer.property.delete'),
url(r'^organizer/(?P<organizer>[^/]+)/giftcards$', organizer.GiftCardListView.as_view(), name='organizer.giftcards'),
url(r'^organizer/(?P<organizer>[^/]+)/giftcard/add$', organizer.GiftCardCreateView.as_view(), name='organizer.giftcard.add'),
url(r'^organizer/(?P<organizer>[^/]+)/giftcard/(?P<giftcard>[^/]+)/$', organizer.GiftCardDetailView.as_view(), name='organizer.giftcard'),

View File

@@ -91,6 +91,10 @@ class MetaDataEditorMixin:
return self.meta_form(
prefix='prop-{}'.format(p.pk),
property=p,
disabled=(
p.protected and
not self.request.user.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings', request=self.request)
),
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)
)

View File

@@ -12,7 +12,7 @@ from django.db.models import (
Count, Max, Min, OuterRef, Prefetch, ProtectedError, Subquery, Sum,
)
from django.db.models.functions import Coalesce, Greatest
from django.forms import DecimalField, inlineformset_factory
from django.forms import DecimalField
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
@@ -269,12 +269,10 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
context['sform'] = self.sform
context['formset'] = self.formset
return context
@transaction.atomic
def form_valid(self, form):
self.save_formset(self.object)
self.sform.save()
change_css = False
if self.sform.has_changed():
@@ -321,38 +319,11 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if form.is_valid() and self.sform.is_valid() and self.formset.is_valid():
if form.is_valid() and self.sform.is_valid():
return self.form_valid(form)
else:
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):
model = Organizer
@@ -1365,3 +1336,94 @@ class GateDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
self.object.delete()
messages.success(request, _('The selected gate has been deleted.'))
return redirect(success_url)
class EventMetaPropertyListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = EventMetaProperty
template_name = 'pretixcontrol/organizers/properties.html'
permission = 'can_change_organizer_settings'
context_object_name = 'properties'
def get_queryset(self):
return self.request.organizer.meta_properties.all()
class EventMetaPropertyCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = EventMetaProperty
template_name = 'pretixcontrol/organizers/property_edit.html'
permission = 'can_change_organizer_settings'
form_class = EventMetaPropertyForm
def get_object(self, queryset=None):
return get_object_or_404(EventMetaProperty, organizer=self.request.organizer, pk=self.kwargs.get('property'))
def get_success_url(self):
return reverse('control:organizer.properties', kwargs={
'organizer': self.request.organizer.slug,
})
def form_valid(self, form):
messages.success(self.request, _('The property has been created.'))
form.instance.organizer = self.request.organizer
ret = super().form_valid(form)
form.instance.log_action('pretix.property.created', user=self.request.user, data={
k: getattr(self.object, k) for k in form.changed_data
})
return ret
def form_invalid(self, form):
messages.error(self.request, _('Your changes could not be saved.'))
return super().form_invalid(form)
class EventMetaPropertyUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = EventMetaProperty
template_name = 'pretixcontrol/organizers/property_edit.html'
permission = 'can_change_organizer_settings'
context_object_name = 'property'
form_class = EventMetaPropertyForm
def get_object(self, queryset=None):
return get_object_or_404(EventMetaProperty, organizer=self.request.organizer, pk=self.kwargs.get('property'))
def get_success_url(self):
return reverse('control:organizer.properties', kwargs={
'organizer': self.request.organizer.slug,
})
def form_valid(self, form):
if form.has_changed():
self.object.log_action('pretix.property.changed', user=self.request.user, data={
k: getattr(self.object, k)
for k in form.changed_data
})
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
def form_invalid(self, form):
messages.error(self.request, _('Your changes could not be saved.'))
return super().form_invalid(form)
class EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DeleteView):
model = EventMetaProperty
template_name = 'pretixcontrol/organizers/property_delete.html'
permission = 'can_change_organizer_settings'
context_object_name = 'property'
def get_object(self, queryset=None):
return get_object_or_404(EventMetaProperty, organizer=self.request.organizer, pk=self.kwargs.get('property'))
def get_success_url(self):
return reverse('control:organizer.properties', kwargs={
'organizer': self.request.organizer.slug,
})
@transaction.atomic
def delete(self, request, *args, **kwargs):
success_url = self.get_success_url()
self.object = self.get_object()
self.object.log_action('pretix.property.deleted', user=self.request.user)
self.object.delete()
messages.success(request, _('The selected property has been deleted.'))
return redirect(success_url)

View File

@@ -167,6 +167,10 @@ class SubEventEditorMixin(MetaDataEditorMixin):
return self.meta_form(
prefix='prop-{}'.format(p.pk),
property=p,
disabled=(
p.protected and
not self.request.user.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings', request=self.request)
),
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)

View File

@@ -1,5 +1,5 @@
[flake8]
ignore = N802,W503,E402,C901,E722,W504,E252,N812,N806
ignore = N802,W503,E402,C901,E722,W504,E252,N812,N806,E741
max-line-length = 160
exclude = migrations,.ropeproject,static,mt940.py,_static,build,make_testdata.py,*/testutils/settings.py,tests/settings.py,pretix/base/models/__init__.py,pretix/base/secretgenerators/pretix_sig1_pb2.py
max-complexity = 11

View File

@@ -164,7 +164,14 @@ def test_event_get(token_client, organizer, event):
@pytest.mark.django_db
def test_event_create(token_client, organizer, event, meta_prop):
def test_event_create(team, token_client, organizer, event, meta_prop):
meta_prop.allowed_values = "Conference\nWorkshop"
meta_prop.save()
team.can_change_organizer_settings = False
team.save()
organizer.meta_properties.create(
name="protected", protected=True
)
resp = token_client.post(
'/api/v1/organizers/{}/events/'.format(organizer.slug),
{
@@ -183,7 +190,8 @@ def test_event_create(token_client, organizer, event, meta_prop):
"location": None,
"slug": "2030",
"meta_data": {
meta_prop.name: "Conference"
meta_prop.name: "Conference",
"protected": "ignored",
},
"seat_category_mapping": {},
"timezone": "Europe/Amsterdam"
@@ -196,6 +204,9 @@ def test_event_create(token_client, organizer, event, meta_prop):
assert organizer.events.get(slug="2030").meta_values.filter(
property__name=meta_prop.name, value="Conference"
).exists()
assert not organizer.events.get(slug="2030").meta_values.filter(
property__name="protected"
).exists()
assert organizer.events.get(slug="2030").plugins == settings.PRETIX_PLUGINS_DEFAULT
assert organizer.events.get(slug="2030").settings.timezone == "Europe/Amsterdam"
@@ -225,6 +236,32 @@ def test_event_create(token_client, organizer, event, meta_prop):
assert resp.status_code == 400
assert resp.content.decode() == '{"meta_data":["Meta data property \'foo\' does not exist."]}'
resp = token_client.post(
'/api/v1/organizers/{}/events/'.format(organizer.slug),
{
"name": {
"de": "Demo Konference 2020 Test",
"en": "Demo Conference 2020 Test"
},
"live": False,
"currency": "EUR",
"date_from": "2017-12-27T10:00:00Z",
"date_to": "2017-12-28T10:00:00Z",
"date_admission": None,
"is_public": False,
"presale_start": None,
"presale_end": None,
"location": None,
"slug": "2020",
"meta_data": {
meta_prop.name: "bar"
}
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"meta_data":["Meta data property \'type\' does not allow value \'bar\'."]}'
resp = token_client.post(
'/api/v1/organizers/{}/events/'.format(organizer.slug),
{

View File

@@ -143,7 +143,14 @@ def test_subevent_list_filter(token_client, organizer, event, subevent):
@pytest.mark.django_db
def test_subevent_create(token_client, organizer, event, subevent, meta_prop, item):
def test_subevent_create(team, token_client, organizer, event, subevent, meta_prop, item):
meta_prop.allowed_values = "Conference\nWorkshop"
meta_prop.save()
team.can_change_organizer_settings = False
team.save()
organizer.meta_properties.create(
name="protected", protected=True
)
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug),
{
@@ -161,7 +168,8 @@ def test_subevent_create(token_client, organizer, event, subevent, meta_prop, it
"item_price_overrides": [],
"variation_price_overrides": [],
"meta_data": {
"type": "Workshop"
"type": "Workshop",
"protected": "ignored",
},
},
format='json'
@@ -172,6 +180,9 @@ def test_subevent_create(token_client, organizer, event, subevent, meta_prop, it
assert subevent.meta_values.filter(
property__name=meta_prop.name, value="Workshop"
).exists()
assert not subevent.meta_values.filter(
property__name="ignored",
).exists()
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug),
@@ -198,6 +209,31 @@ def test_subevent_create(token_client, organizer, event, subevent, meta_prop, it
assert resp.status_code == 400
assert resp.content.decode() == '{"meta_data":["Meta data property \'foo\' does not exist."]}'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug),
{
"name": {
"de": "Demo Subevent 2020 Test",
"en": "Demo Subevent 2020 Test"
},
"active": False,
"date_from": "2017-12-27T10:00:00Z",
"date_to": "2017-12-28T10:00:00Z",
"date_admission": None,
"presale_start": None,
"presale_end": None,
"location": None,
"item_price_overrides": [],
"variation_price_overrides": [],
"meta_data": {
meta_prop.name: "bar"
},
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"meta_data":["Meta data property \'type\' does not allow value \'bar\'."]}'
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug),
{

View File

@@ -148,6 +148,10 @@ organizer_urls = [
'organizer/abc/gate/add',
'organizer/abc/gate/1/edit',
'organizer/abc/gate/1/delete',
'organizer/abc/properties',
'organizer/abc/property/add',
'organizer/abc/property/1/edit',
'organizer/abc/property/1/delete',
'organizer/abc/webhooks',
'organizer/abc/webhook/add',
'organizer/abc/webhook/1/edit',
@@ -428,6 +432,10 @@ organizer_permission_urls = [
("can_change_organizer_settings", "organizer/dummy/gate/add", 200),
("can_change_organizer_settings", "organizer/dummy/gate/1/edit", 404),
("can_change_organizer_settings", "organizer/dummy/gate/1/delete", 404),
("can_change_organizer_settings", "organizer/dummy/properties", 200),
("can_change_organizer_settings", "organizer/dummy/property/add", 200),
("can_change_organizer_settings", "organizer/dummy/property/1/edit", 404),
("can_change_organizer_settings", "organizer/dummy/property/1/delete", 404),
("can_manage_gift_cards", "organizer/dummy/giftcards", 200),
("can_manage_gift_cards", "organizer/dummy/giftcard/add", 200),
("can_manage_gift_cards", "organizer/dummy/giftcard/1/", 404),