diff --git a/doc/user/events/widget.rst b/doc/user/events/widget.rst
index f19f3cd285..64a60e4952 100644
--- a/doc/user/events/widget.rst
+++ b/doc/user/events/widget.rst
@@ -196,6 +196,10 @@ settings. For example, if you set up a meta data property called "Promoted" that
+If you have enabled public filters in your meta data attribute configuration, a filter formshows up. To disable, use::
+
+
+
pretix Button
-------------
diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py
index 6bfed06ad1..5ac98d62b9 100644
--- a/src/pretix/api/serializers/event.py
+++ b/src/pretix/api/serializers/event.py
@@ -230,8 +230,8 @@ class EventSerializer(I18nAwareModelSerializer):
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()]:
+ if self.meta_properties[key].choices:
+ if v not in self.meta_properties[key].choice_keys:
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
return value
@@ -528,8 +528,8 @@ class SubEventSerializer(I18nAwareModelSerializer):
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()]:
+ if self.meta_properties[key].choices:
+ if v not in self.meta_properties[key].choice_keys:
raise ValidationError(_('Meta data property \'{name}\' does not allow value \'{value}\'.').format(name=key, value=v))
return value
@@ -705,6 +705,7 @@ class EventSettingsSerializer(SettingsSerializer):
'frontpage_subevent_ordering',
'event_list_type',
'event_list_available_only',
+ 'event_list_filters',
'event_calendar_future_only',
'frontpage_text',
'event_info_text',
diff --git a/src/pretix/base/migrations/0250_eventmetaproperty_filter_public.py b/src/pretix/base/migrations/0250_eventmetaproperty_filter_public.py
new file mode 100644
index 0000000000..19b4244036
--- /dev/null
+++ b/src/pretix/base/migrations/0250_eventmetaproperty_filter_public.py
@@ -0,0 +1,52 @@
+# Generated by Django 4.2.4 on 2023-10-31 10:08
+import i18nfield.fields
+from django.db import migrations, models
+
+import pretix.helpers.json
+
+
+def convert_allowed_values(apps, schema_editor):
+ EventMetaProperty = apps.get_model('pretixbase', 'EventMetaProperty')
+ for emp in EventMetaProperty.objects.filter(allowed_values__isnull=False):
+ emp.choices = [
+ {"key": _v.strip(), "label": {"en": _v.strip()}}
+ for _v in emp.allowed_values.splitlines()
+ ]
+ emp.save(update_fields=["choices"])
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("pretixbase", "0249_hidden_if_item_available"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="eventmetaproperty",
+ name="filter_public",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name="eventmetaproperty",
+ name="public_label",
+ field=i18nfield.fields.I18nCharField(null=True),
+ ),
+ migrations.AddField(
+ model_name="eventmetaproperty",
+ name="position",
+ field=models.IntegerField(default=0),
+ ),
+ migrations.AddField(
+ model_name="eventmetaproperty",
+ name="choices",
+ field=models.JSONField(null=True, encoder=pretix.helpers.json.CustomJSONEncoder),
+ ),
+ migrations.RunPython(
+ convert_allowed_values,
+ migrations.RunPython.noop
+ ),
+ migrations.RemoveField(
+ model_name="eventmetaproperty",
+ name="allowed_values",
+ ),
+ ]
diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py
index deaad5a811..1cfa84e1d4 100644
--- a/src/pretix/base/models/event.py
+++ b/src/pretix/base/models/event.py
@@ -20,7 +20,6 @@
# .
#
-import logging
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at .
#
@@ -33,6 +32,7 @@ import logging
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
+import logging
import os
import string
import uuid
@@ -71,7 +71,7 @@ from pretix.base.validators import EventSlugBanlistValidator
from pretix.helpers.database import GroupConcat
from pretix.helpers.daterange import daterange
from pretix.helpers.hierarkey import clean_filename
-from pretix.helpers.json import safe_string
+from pretix.helpers.json import CustomJSONEncoder, safe_string
from pretix.helpers.thumb import get_thumbnail
from ..settings import settings_hierarkey
@@ -1647,26 +1647,40 @@ class EventMetaProperty(LoggedModel):
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(
+ choices = models.JSONField(
null=True, blank=True,
+ encoder=CustomJSONEncoder,
verbose_name=_("Valid values"),
- help_text=_("If you keep this empty, any value is allowed. Otherwise, enter one possible value per line.")
+ )
+ filter_public = models.BooleanField(
+ default=False, verbose_name=_("Show filter option to customers"),
+ help_text=_("This field will be shown to filter events in the public event list and calendar.")
+ )
+ public_label = I18nCharField(
+ verbose_name=_("Public name"),
+ null=True, blank=True,
)
filter_allowed = models.BooleanField(
default=True, verbose_name=_("Can be used for filtering"),
help_text=_("This field will be shown to filter events or reports in the backend, and it can also be used "
"for hidden filter parameters in the frontend (e.g. using the widget).")
)
+ position = models.IntegerField(
+ default=0
+ )
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 Meta:
- ordering = ("name",)
+ ordering = ("position", "name",)
+
+ @property
+ def choice_keys(self):
+ if self.choices:
+ return [v["key"] for v in self.choices]
class EventMetaValue(LoggedModel):
diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
index dc945ff38f..069db06aff 100644
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -1606,6 +1606,16 @@ DEFAULTS = {
help_text=_('If your event series has more than 50 dates in the future, only the month or week calendar can be used.')
),
},
+ 'event_list_filters': {
+ 'default': 'True',
+ 'type': bool,
+ 'form_class': forms.BooleanField,
+ 'serializer_class': serializers.BooleanField,
+ 'form_kwargs': dict(
+ label=_("Show filter options for calendar or list view"),
+ help_text=_("You can set up possible filters as meta properties in your organizer settings.")
+ )
+ },
'event_list_available_only': {
'default': 'False',
'type': bool,
diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py
index b326eb8f16..c84aa1fec2 100644
--- a/src/pretix/control/forms/event.py
+++ b/src/pretix/control/forms/event.py
@@ -316,12 +316,12 @@ class EventMetaValueForm(forms.ModelForm):
self.property = kwargs.pop('property')
self.disabled = kwargs.pop('disabled')
super().__init__(*args, **kwargs)
- if self.property.allowed_values:
+ if self.property.choices:
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()],
+ ] + [(a.strip(), a.strip()) for a in self.property.choice_keys],
)
else:
self.fields['value'].label = self.property.name
@@ -558,6 +558,7 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
'low_availability_percentage',
'event_list_type',
'event_list_available_only',
+ 'event_list_filters',
'event_calendar_future_only',
'frontpage_text',
'event_info_text',
@@ -645,6 +646,7 @@ class EventSettingsForm(EventSettingsValidationMixin, SettingsForm):
del self.fields['frontpage_subevent_ordering']
del self.fields['event_list_type']
del self.fields['event_list_available_only']
+ del self.fields['event_list_filters']
del self.fields['event_calendar_future_only']
# create "virtual" fields for better UX when editing _asked and _required fields
diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py
index befad313a4..631316532a 100644
--- a/src/pretix/control/forms/organizer.py
+++ b/src/pretix/control/forms/organizer.py
@@ -39,7 +39,7 @@ from django import forms
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import Q
-from django.forms import inlineformset_factory
+from django.forms import formset_factory, inlineformset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.crypto import get_random_string
@@ -48,7 +48,7 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes.forms import SafeModelChoiceField
from i18nfield.forms import (
- I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
+ I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput,
)
from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones
@@ -195,14 +195,50 @@ class SafeOrderPositionChoiceField(forms.ModelChoiceField):
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
-class EventMetaPropertyForm(forms.ModelForm):
+class EventMetaPropertyForm(I18nModelForm):
class Meta:
model = EventMetaProperty
- fields = ['name', 'default', 'required', 'protected', 'allowed_values', 'filter_allowed']
+ fields = ['name', 'default', 'required', 'protected', 'filter_public', 'public_label', 'filter_allowed']
widgets = {
- 'default': forms.TextInput()
+ 'default': forms.TextInput(),
}
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['public_label'].widget.attrs['data-display-dependency'] = '#id_filter_public'
+
+
+class EventMetaPropertyAllowedValueForm(I18nForm):
+ key = forms.CharField(
+ label=_('Internal name'),
+ max_length=250,
+ required=True
+ )
+ label = I18nFormField(
+ label=_('Public name'),
+ required=False,
+ widget=I18nTextInput,
+ widget_kwargs=dict(attrs={
+ 'placeholder': _('Public name'),
+ })
+ )
+
+
+class I18nBaseFormSet(I18nFormSetMixin, forms.BaseFormSet):
+ # compatibility shim for django-i18nfield library
+
+ def __init__(self, *args, **kwargs):
+ self.organizer = kwargs.pop('organizer', None)
+ if self.organizer:
+ kwargs['locales'] = self.organizer.settings.get('locales')
+ super().__init__(*args, **kwargs)
+
+
+EventMetaPropertyAllowedValueFormSet = formset_factory(
+ EventMetaPropertyAllowedValueForm, formset=I18nBaseFormSet,
+ can_order=True, can_delete=True, extra=0
+)
+
class MembershipTypeForm(I18nModelForm):
class Meta:
diff --git a/src/pretix/control/forms/subevents.py b/src/pretix/control/forms/subevents.py
index ef741e6756..6c3145d90c 100644
--- a/src/pretix/control/forms/subevents.py
+++ b/src/pretix/control/forms/subevents.py
@@ -393,12 +393,12 @@ class SubEventMetaValueForm(forms.ModelForm):
self.default = kwargs.pop('default', None)
self.disabled = kwargs.pop('disabled', False)
super().__init__(*args, **kwargs)
- if self.property.allowed_values:
+ if self.property.choices:
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()],
+ ] + [(a.strip(), a.strip()) for a in self.property.choice_keys],
)
else:
self.fields['value'].label = self.property.name
diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html
index 25afd4ba64..9b908c7fab 100644
--- a/src/pretix/control/templates/pretixcontrol/event/settings.html
+++ b/src/pretix/control/templates/pretixcontrol/event/settings.html
@@ -316,6 +316,9 @@
{% if sform.event_list_available_only %}
{% bootstrap_field sform.event_list_available_only layout="control" %}
{% endif %}
+ {% if sform.event_list_filters %}
+ {% bootstrap_field sform.event_list_filters layout="control" %}
+ {% endif %}
{% if sform.event_calendar_future_only %}
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
{% endif %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/properties.html b/src/pretix/control/templates/pretixcontrol/organizers/properties.html
index 84a4b0da63..321458229f 100644
--- a/src/pretix/control/templates/pretixcontrol/organizers/properties.html
+++ b/src/pretix/control/templates/pretixcontrol/organizers/properties.html
@@ -14,29 +14,56 @@
{% trans "Create a new property" %}
-