mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Event settings: Extend product metadata (Z#23116647) (#3241)
Co-authored-by: Richard Schreiber <schreiber@rami.io> Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
@@ -50,7 +50,9 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
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.models.items import (
|
||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||
)
|
||||
from pretix.base.services.seating import (
|
||||
SeatProtected, generate_seats, validate_plan_change,
|
||||
)
|
||||
@@ -904,3 +906,23 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
else []
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class MultiLineStringField(serializers.Field):
|
||||
|
||||
def to_representation(self, value):
|
||||
return [v.strip() for v in value.splitlines()]
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, list) and len(data) > 0:
|
||||
return "\n".join(data)
|
||||
else:
|
||||
raise ValidationError('Invalid data type.')
|
||||
|
||||
|
||||
class ItemMetaPropertiesSerializer(I18nAwareModelSerializer):
|
||||
allowed_values = MultiLineStringField(allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = ItemMetaProperty
|
||||
fields = ('id', 'name', 'default', 'required', 'allowed_values')
|
||||
|
||||
@@ -89,6 +89,7 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet)
|
||||
event_router.register(r'cartpositions', cart.CartPositionViewSet)
|
||||
event_router.register(r'exporters', exporters.EventExportersViewSet, basename='exporters')
|
||||
event_router.register(r'shredders', shredders.EventShreddersViewSet, basename='shredders')
|
||||
event_router.register(r'item_meta_properties', event.ItemMetaPropertiesViewSet)
|
||||
|
||||
checkinlist_router = routers.DefaultRouter()
|
||||
checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos')
|
||||
|
||||
@@ -47,11 +47,13 @@ from pretix.api.auth.permission import EventCRUDPermission
|
||||
from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.event import (
|
||||
CloneEventSerializer, DeviceEventSettingsSerializer, EventSerializer,
|
||||
EventSettingsSerializer, SubEventSerializer, TaxRuleSerializer,
|
||||
EventSettingsSerializer, ItemMetaPropertiesSerializer, SubEventSerializer,
|
||||
TaxRuleSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
CartPosition, Device, Event, SeatCategoryMapping, TaxRule, TeamAPIToken,
|
||||
CartPosition, Device, Event, ItemMetaProperty, SeatCategoryMapping,
|
||||
TaxRule, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
@@ -522,6 +524,54 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ItemMetaPropertiesSerializer
|
||||
queryset = ItemMetaProperty.objects.none()
|
||||
write_permission = 'can_change_event_settings'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.item_meta_properties.all()
|
||||
return qs
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['organizer'] = self.request.organizer
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.item_meta_property.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'id': instance.pk}
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_create(self, serializer):
|
||||
inst = serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.item_meta_property.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
inst = serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.item_meta_property.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
|
||||
class EventSettingsView(views.APIView):
|
||||
permission = None
|
||||
write_permission = 'can_change_event_settings'
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.19 on 2023-05-25 15:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0240_auto_20230516_1119'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='itemmetaproperty',
|
||||
name='allowed_values',
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itemmetaproperty',
|
||||
name='required',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -2001,6 +2001,15 @@ class ItemMetaProperty(LoggedModel):
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
default = models.TextField(blank=True)
|
||||
required = models.BooleanField(
|
||||
default=False, verbose_name=_("Required for products"),
|
||||
help_text=_("If checked, this property must be set in each product. Does not apply if a default value is set.")
|
||||
)
|
||||
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.")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ("name",)
|
||||
|
||||
@@ -1674,7 +1674,7 @@ QuickSetupProductFormSet = formset_factory(
|
||||
|
||||
class ItemMetaPropertyForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ['name', 'default']
|
||||
fields = ['name', 'default', 'required', 'allowed_values']
|
||||
widgets = {
|
||||
'default': forms.TextInput()
|
||||
}
|
||||
|
||||
@@ -1102,16 +1102,26 @@ class ItemMetaValueForm(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
|
||||
self.fields['value'].widget.attrs['data-typeahead-url'] = (
|
||||
reverse('control:event.items.meta.typeahead', kwargs={
|
||||
'organizer': self.property.event.organizer.slug,
|
||||
'event': self.property.event.slug
|
||||
}) + '?' + urlencode({
|
||||
'property': self.property.name,
|
||||
})
|
||||
)
|
||||
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:event.items.meta.typeahead', kwargs={
|
||||
'organizer': self.property.event.organizer.slug,
|
||||
'event': self.property.event.slug
|
||||
}) + '?' + urlencode({
|
||||
'property': self.property.name,
|
||||
})
|
||||
)
|
||||
self.fields['value'].required = self.property.required and not self.property.default
|
||||
|
||||
class Meta:
|
||||
model = ItemMetaValue
|
||||
|
||||
@@ -485,6 +485,9 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.item.bundles.added': _('A bundled item has been added to this product.'),
|
||||
'pretix.event.item.bundles.removed': _('A bundled item has been removed from this product.'),
|
||||
'pretix.event.item.bundles.changed': _('A bundled item has been changed on this product.'),
|
||||
'pretix.event.item_meta_property.added': _('A meta property has been added to this event.'),
|
||||
'pretix.event.item_meta_property.deleted': _('A meta property has been removed from this event.'),
|
||||
'pretix.event.item_meta_property.changed': _('A meta property has been changed on this event.'),
|
||||
'pretix.event.quota.added': _('The quota has been added.'),
|
||||
'pretix.event.quota.deleted': _('The quota has been deleted.'),
|
||||
'pretix.event.quota.changed': _('The quota has been changed.'),
|
||||
|
||||
@@ -378,41 +378,55 @@
|
||||
{% bootstrap_formset_errors item_meta_property_formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in item_meta_property_formset %}
|
||||
<div class="row formset-row" data-formset-form>
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<h3 class="panel-title">{% trans "Property" %}</h3>
|
||||
</div>
|
||||
<div class="col-sm-4 text-right flip">
|
||||
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% 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>
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.default layout="control" %}
|
||||
{% bootstrap_field form.required layout="control" %}
|
||||
{% bootstrap_field form.allowed_values layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="row formset-row" data-formset-form>
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ item_meta_property_formset.empty_form.id }}
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.name layout='inline' form_group_class="" %}
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<h3 class="panel-title">{% trans "Property" %}</h3>
|
||||
</div>
|
||||
<div class="col-sm-4 text-right flip">
|
||||
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5 col-lg-6">
|
||||
{% bootstrap_field item_meta_property_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 class="panel-body form-horizontal">
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.name layout="control" %}
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.default layout="control" %}
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.required layout="control" %}
|
||||
{% bootstrap_field item_meta_property_formset.empty_form.allowed_values layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
|
||||
@@ -139,6 +139,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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" error_types="all" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</fieldset>
|
||||
{% if form.quota_option %}
|
||||
<fieldset>
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{% bootstrap_form form layout="inline" %}
|
||||
{% bootstrap_form form layout="inline" error_types="all" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -282,9 +282,19 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
|
||||
if form in self.item_meta_property_formset.deleted_forms:
|
||||
if not form.instance.pk:
|
||||
continue
|
||||
form.instance.log_action(
|
||||
'pretix.event.item_meta_property.deleted',
|
||||
user=self.request.user,
|
||||
data=form.cleaned_data
|
||||
)
|
||||
form.instance.delete()
|
||||
form.instance.pk = None
|
||||
elif form.has_changed():
|
||||
form.instance.log_action(
|
||||
'pretix.event.item_meta_property.changed',
|
||||
user=self.request.user,
|
||||
data=form.cleaned_data
|
||||
)
|
||||
form.save()
|
||||
|
||||
for form in self.item_meta_property_formset.extra_forms:
|
||||
@@ -294,6 +304,11 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
|
||||
continue
|
||||
form.instance.event = obj
|
||||
form.save()
|
||||
form.instance.log_action(
|
||||
'pretix.event.item_meta_property.added',
|
||||
user=self.request.user,
|
||||
data=form.cleaned_data
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def confirm_texts_formset(self):
|
||||
|
||||
@@ -1213,7 +1213,7 @@ class MetaDataEditorMixin:
|
||||
f.instance.delete()
|
||||
|
||||
|
||||
class ItemCreate(EventPermissionRequiredMixin, CreateView):
|
||||
class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
|
||||
form_class = ItemCreateForm
|
||||
template_name = 'pretixcontrol/item/create.html'
|
||||
permission = 'can_change_items'
|
||||
@@ -1274,6 +1274,11 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['meta_forms'] = self.meta_forms
|
||||
return ctx
|
||||
|
||||
|
||||
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
|
||||
form_class = ItemUpdateForm
|
||||
|
||||
Reference in New Issue
Block a user