Add meta_data for items (#1576)

* PoC for ItemMetaProperties/Values

* Missing is_valid

* ItemMetaProperties/Values in editable via API, cloneable

* Tests

* Add Docs

* Fix import order

* Fix another import sorting...

* Typeahead for ItemMetaValues

* Test for editing event-objects

* Fix typeahead permission checks

* Further access restriction

Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
This commit is contained in:
Martin Gross
2020-02-26 15:06:25 +01:00
committed by GitHub
parent dd1e5fa929
commit 76aaf61e19
22 changed files with 573 additions and 25 deletions

View File

@@ -34,6 +34,19 @@ class MetaDataField(Field):
}
class MetaPropertyField(Field):
def to_representation(self, value):
return {
v.name: v.default for v in value.item_meta_properties.all()
}
def to_internal_value(self, data):
return {
'item_meta_properties': data
}
class SeatCategoryMappingField(Field):
def to_representation(self, value):
@@ -77,6 +90,7 @@ class TimeZoneField(ChoiceField):
class EventSerializer(I18nAwareModelSerializer):
meta_data = MetaDataField(required=False, source='*')
item_meta_properties = MetaPropertyField(required=False, source='*')
plugins = PluginsField(required=False, source='*')
seat_category_mapping = SeatCategoryMappingField(source='*', required=False)
timezone = TimeZoneField(required=False, choices=[(a, a) for a in common_timezones])
@@ -86,7 +100,7 @@ class EventSerializer(I18nAwareModelSerializer):
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
'plugins', 'seat_category_mapping', 'timezone')
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties')
def validate(self, data):
data = super().validate(data)
@@ -131,6 +145,12 @@ class EventSerializer(I18nAwareModelSerializer):
raise ValidationError(_('Meta data property \'{name}\' does not exist.').format(name=key))
return value
@cached_property
def item_meta_props(self):
return {
p.name: p for p in self.context['request'].event.item_meta_properties.all()
}
def validate_seating_plan(self, value):
if value and value.organizer != self.context['request'].organizer:
raise ValidationError('Invalid seating plan.')
@@ -172,6 +192,7 @@ class EventSerializer(I18nAwareModelSerializer):
@transaction.atomic
def create(self, validated_data):
meta_data = validated_data.pop('meta_data', None)
item_meta_properties = validated_data.pop('item_meta_properties', None)
validated_data.pop('seat_category_mapping', None)
plugins = validated_data.pop('plugins', settings.PRETIX_PLUGINS_DEFAULT.split(','))
tz = validated_data.pop('timezone', None)
@@ -188,6 +209,15 @@ class EventSerializer(I18nAwareModelSerializer):
value=value
)
# Item Meta properties
if item_meta_properties is not None:
for key, value in item_meta_properties.items():
event.item_meta_properties.create(
name=key,
default=value,
event=event
)
# Seats
if event.seating_plan:
generate_seats(event, None, event.seating_plan, {})
@@ -202,6 +232,7 @@ class EventSerializer(I18nAwareModelSerializer):
@transaction.atomic
def update(self, instance, validated_data):
meta_data = validated_data.pop('meta_data', None)
item_meta_properties = validated_data.pop('item_meta_properties', None)
plugins = validated_data.pop('plugins', None)
seat_category_mapping = validated_data.pop('seat_category_mapping', None)
tz = validated_data.pop('timezone', None)
@@ -228,6 +259,26 @@ class EventSerializer(I18nAwareModelSerializer):
if prop.name not in meta_data:
current_object.delete()
# Item Meta properties
if item_meta_properties is not None:
current = [imp for imp in event.item_meta_properties.all()]
for key, value in item_meta_properties.items():
prop = self.item_meta_props.get(key)
if prop in current:
prop.default = value
prop.save()
else:
prop = event.item_meta_properties.create(
name=key,
default=value,
event=event
)
current.append(prop)
for prop in current:
if prop.name not in list(item_meta_properties.keys()):
prop.delete()
# Seats
if seat_category_mapping is not None or ('seating_plan' in validated_data and validated_data['seating_plan'] is None):
current_mappings = {

View File

@@ -2,13 +2,15 @@ from decimal import Decimal
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from pretix.api.serializers.event import MetaDataField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation, Question,
QuestionOption, Quota,
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
Question, QuestionOption, Quota,
)
@@ -110,6 +112,7 @@ class ItemSerializer(I18nAwareModelSerializer):
bundles = InlineItemBundleSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False)
tax_rate = ItemTaxRateField(source='*', read_only=True)
meta_data = MetaDataField(required=False, source='*')
class Meta:
model = Item
@@ -119,7 +122,7 @@ class ItemSerializer(I18nAwareModelSerializer):
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard')
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data')
read_only_fields = ('has_variations', 'picture')
def validate(self, data):
@@ -167,18 +170,65 @@ class ItemSerializer(I18nAwareModelSerializer):
ItemAddOn.clean_max_min_count(addon_data['max_count'], addon_data['min_count'])
return value
@cached_property
def item_meta_properties(self):
return {
p.name: p for p in self.context['request'].event.item_meta_properties.all()
}
def validate_meta_data(self, value):
for key in value['meta_data'].keys():
if key not in self.item_meta_properties:
raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key))
return value
@transaction.atomic
def create(self, validated_data):
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
meta_data = validated_data.pop('meta_data', None)
item = Item.objects.create(**validated_data)
for variation_data in variations_data:
ItemVariation.objects.create(item=item, **variation_data)
for addon_data in addons_data:
ItemAddOn.objects.create(base_item=item, **addon_data)
for bundle_data in bundles_data:
ItemBundle.objects.create(base_item=item, **bundle_data)
# Meta data
if meta_data is not None:
for key, value in meta_data.items():
ItemMetaValue.objects.create(
property=self.item_meta_properties.get(key),
value=value,
item=item
)
return item
def update(self, instance, validated_data):
meta_data = validated_data.pop('meta_data', None)
item = super().update(instance, validated_data)
# Meta data
if meta_data is not None:
current = {mv.property: mv for mv in item.meta_values.select_related('property')}
for key, value in meta_data.items():
prop = self.item_meta_properties.get(key)
if prop in current:
current[prop].value = value
current[prop].save()
else:
item.meta_values.create(
property=self.item_meta_properties.get(key),
value=value
)
for prop, current_object in current.items():
if prop.name not in meta_data:
current_object.delete()
return item