diff --git a/doc/api/resources/item_variations.rst b/doc/api/resources/item_variations.rst index c5ce44411b..2482e17158 100644 --- a/doc/api/resources/item_variations.rst +++ b/doc/api/resources/item_variations.rst @@ -43,8 +43,13 @@ available_until datetime The last date t hide_without_voucher boolean If ``true``, this variation is only shown during the voucher redemption process, but not in the normal shop frontend. +meta_data object Values set for event-specific meta data parameters. ===================================== ========================== ======================================================= +.. versionchanged:: 4.16 + + The ``meta_data`` attribute has been added. + Endpoints --------- @@ -94,6 +99,7 @@ Endpoints "default_price": "223.00", "price": 223.0, "original_price": null, + "meta_data": {} }, { "id": 3, @@ -108,7 +114,8 @@ Endpoints "description": {}, "position": 1, "default_price": null, - "price": 15.0 + "price": 15.0, + "meta_data": {} } ] } @@ -161,7 +168,8 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, - "position": 0 + "position": 0, + "meta_data": {} } :param organizer: The ``slug`` field of the organizer to fetch @@ -198,7 +206,8 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, - "position": 0 + "position": 0, + "meta_data": {} } **Example response**: @@ -225,7 +234,8 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, - "position": 0 + "position": 0, + "meta_data": {} } :param organizer: The ``slug`` field of the organizer of the event/item to create a variation for @@ -283,7 +293,8 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, - "position": 1 + "position": 1, + "meta_data": {} } :param organizer: The ``slug`` field of the organizer to modify diff --git a/doc/api/resources/items.rst b/doc/api/resources/items.rst index 9ca964f911..0e060764cc 100644 --- a/doc/api/resources/items.rst +++ b/doc/api/resources/items.rst @@ -123,6 +123,7 @@ variations list of objects A list with one ├ hide_without_voucher boolean If ``true``, this variation is only shown during the voucher redemption process, but not in the normal shop frontend. +├ meta_data object Values set for event-specific meta data parameters. └ position integer An integer, used for sorting addons list of objects Definition of add-ons that can be chosen for this item. Only writable during creation, @@ -155,6 +156,10 @@ meta_data object Values set for The attributes ``require_membership_hidden`` attribute has been added. +.. versionchanged:: 4.16 + + The ``variations[x].meta_data`` attribute has been added. + Notes ----- @@ -247,6 +252,7 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, + "meta_data": {}, "position": 0 }, { @@ -262,6 +268,7 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, + "meta_data": {}, "position": 1 } ], @@ -361,6 +368,7 @@ Endpoints "available_from": null, "available_until": null, "hide_without_voucher": false, + "meta_data": {}, "position": 0 }, { @@ -376,6 +384,7 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, + "meta_data": {}, "position": 1 } ], @@ -455,6 +464,7 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, + "meta_data": {}, "position": 0 }, { @@ -470,6 +480,7 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, + "meta_data": {}, "position": 1 } ], @@ -538,6 +549,7 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, + "meta_data": {}, "position": 0 }, { @@ -553,6 +565,7 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, + "meta_data": {}, "position": 1 } ], @@ -652,6 +665,7 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, + "meta_data": {}, "position": 0 }, { @@ -667,6 +681,7 @@ Endpoints "available_until": null, "hide_without_voucher": false, "description": null, + "meta_data": {}, "position": 1 } ], diff --git a/src/pretix/api/serializers/item.py b/src/pretix/api/serializers/item.py index a1c22d6838..548db655ff 100644 --- a/src/pretix/api/serializers/item.py +++ b/src/pretix/api/serializers/item.py @@ -47,13 +47,14 @@ from pretix.api.serializers.fields import UploadedFileField from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.base.models import ( Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation, - Question, QuestionOption, Quota, + ItemVariationMetaValue, Question, QuestionOption, Quota, ) class InlineItemVariationSerializer(I18nAwareModelSerializer): price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10, coerce_to_string=True) + meta_data = MetaDataField(required=False, source='*') class Meta: model = ItemVariation @@ -61,16 +62,23 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer): 'position', 'default_price', 'price', 'original_price', 'require_approval', 'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until', - 'sales_channels', 'hide_without_voucher',) + 'sales_channels', 'hide_without_voucher', 'meta_data') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['require_membership_types'].queryset = lazy(lambda: self.context['event'].organizer.membership_types.all(), QuerySet) + def validate_meta_data(self, value): + for key in value['meta_data'].keys(): + if key not in self.parent.parent.item_meta_properties: + raise ValidationError(_('Item meta data property \'{name}\' does not exist.').format(name=key)) + return value + class ItemVariationSerializer(I18nAwareModelSerializer): price = serializers.DecimalField(read_only=True, decimal_places=2, max_digits=10, coerce_to_string=True) + meta_data = MetaDataField(required=False, source='*') class Meta: model = ItemVariation @@ -78,12 +86,63 @@ class ItemVariationSerializer(I18nAwareModelSerializer): 'position', 'default_price', 'price', 'original_price', 'require_approval', 'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until', - 'sales_channels', 'hide_without_voucher',) + 'sales_channels', 'hide_without_voucher', 'meta_data') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['require_membership_types'].queryset = self.context['event'].organizer.membership_types.all() + @transaction.atomic + def create(self, validated_data): + meta_data = validated_data.pop('meta_data', None) + variation = ItemVariation.objects.create(**validated_data) + + # Meta data + if meta_data is not None: + for key, value in meta_data.items(): + ItemVariationMetaValue.objects.create( + property=self.item_meta_properties.get(key), + value=value, + variation=variation + ) + return variation + + @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 + + def update(self, instance, validated_data): + meta_data = validated_data.pop('meta_data', None) + variation = super().update(instance, validated_data) + + # Meta data + if meta_data is not None: + current = {mv.property: mv for mv in variation.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: + variation.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 variation + class InlineItemBundleSerializer(serializers.ModelSerializer): class Meta: @@ -263,9 +322,19 @@ class ItemSerializer(I18nAwareModelSerializer): for variation_data in variations_data: require_membership_types = variation_data.pop('require_membership_types', []) + var_meta_data = variation_data.pop('meta_data', {}) v = ItemVariation.objects.create(item=item, **variation_data) if require_membership_types: v.require_membership_types.add(*require_membership_types) + + if var_meta_data is not None: + for key, value in var_meta_data.items(): + ItemVariationMetaValue.objects.create( + property=self.item_meta_properties.get(key), + value=value, + variation=v + ) + for addon_data in addons_data: ItemAddOn.objects.create(base_item=item, **addon_data) for bundle_data in bundles_data: diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index e99030c565..be189091a7 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -371,10 +371,19 @@ class PdfDataSerializer(serializers.Field): for k, v in ev._cached_meta_data.items(): res['meta:' + k] = v - if not hasattr(instance.item, '_cached_meta_data'): - instance.item._cached_meta_data = instance.item.meta_data - for k, v in instance.item._cached_meta_data.items(): - res['itemmeta:' + k] = v + if instance.variation_id: + print(instance, instance.variation, instance.variation_id, instance.item) + if not hasattr(instance.variation, '_cached_meta_data'): + instance.variation.item = instance.item # saves some database lookups + instance.variation._cached_meta_data = instance.variation.meta_data + print(instance.variation._cached_meta_data.items()) + for k, v in instance.variation._cached_meta_data.items(): + res['itemmeta:' + k] = v + else: + if not hasattr(instance.item, '_cached_meta_data'): + instance.item._cached_meta_data = instance.item.meta_data + for k, v in instance.item._cached_meta_data.items(): + res['itemmeta:' + k] = v res['images'] = {} diff --git a/src/pretix/api/views/item.py b/src/pretix/api/views/item.py index 24ae8ea1a6..3003aa7b96 100644 --- a/src/pretix/api/views/item.py +++ b/src/pretix/api/views/item.py @@ -84,7 +84,9 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet): def get_queryset(self): return self.request.event.items.select_related('tax_rule').prefetch_related( - 'variations', 'addons', 'bundles', 'meta_values' + 'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property', + 'variations__meta_values', 'variations__meta_values__property', + 'require_membership_types', 'variations__require_membership_types', ).all() def perform_create(self, serializer): @@ -147,7 +149,11 @@ class ItemVariationViewSet(viewsets.ModelViewSet): return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event) def get_queryset(self): - return self.item.variations.all() + return self.item.variations.all().prefetch_related( + 'meta_values', + 'meta_values__property', + 'require_membership_types' + ) def get_serializer_context(self): ctx = super().get_serializer_context() diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index f7e29b5637..6a824e064c 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -65,9 +65,10 @@ from pretix.api.views import RichOrderingFilter from pretix.base.i18n import language from pretix.base.models import ( CachedCombinedTicket, CachedTicket, Checkin, Device, EventMetaValue, - Invoice, InvoiceAddress, ItemMetaValue, Order, OrderFee, OrderPayment, - OrderPosition, OrderRefund, Quota, SubEvent, SubEventMetaValue, TaxRule, - TeamAPIToken, generate_secret, + Invoice, InvoiceAddress, ItemMetaValue, ItemVariation, + ItemVariationMetaValue, Order, OrderFee, OrderPayment, OrderPosition, + OrderRefund, Quota, SubEvent, SubEventMetaValue, TaxRule, TeamAPIToken, + generate_secret, ) from pretix.base.models.orders import QuestionAnswer, RevokedTicketSecret from pretix.base.payment import PaymentException @@ -232,7 +233,9 @@ class OrderViewSet(viewsets.ModelViewSet): Prefetch('item', queryset=self.request.event.items.prefetch_related( Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached') )), - 'variation', + Prefetch('variation', queryset=ItemVariation.objects.prefetch_related( + Prefetch('meta_values', ItemVariationMetaValue.objects.select_related('property'), to_attr='meta_values_cached') + )), 'answers', 'answers__options', 'answers__question', 'item__category', 'addon_to__answers', 'addon_to__answers__options', 'addon_to__answers__question', @@ -999,7 +1002,11 @@ class OrderPositionViewSet(viewsets.ModelViewSet): Prefetch('meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached') )), - 'variation', 'answers', 'answers__options', 'answers__question', + Prefetch('variation', queryset=self.request.event.items.prefetch_related( + Prefetch('meta_values', ItemVariationMetaValue.objects.select_related('property'), + to_attr='meta_values_cached') + )), + 'answers', 'answers__options', 'answers__question', 'item__category', Prefetch('subevent', queryset=self.request.event.subevents.prefetch_related( Prefetch('meta_values', to_attr='meta_values_cached', diff --git a/src/pretix/base/exporters/items.py b/src/pretix/base/exporters/items.py index 6988d8b4eb..596fe0fdd1 100644 --- a/src/pretix/base/exporters/items.py +++ b/src/pretix/base/exporters/items.py @@ -29,7 +29,7 @@ from openpyxl.utils import get_column_letter from ...helpers.safe_openpyxl import SafeCell from ..channels import get_all_sales_channels from ..exporter import ListExporter -from ..models import ItemMetaValue +from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue from ..signals import register_data_exporters @@ -106,18 +106,27 @@ class ItemDataExporter(ListExporter): yield row for i in self.event.items.prefetch_related( - 'variations', Prefetch( 'meta_values', ItemMetaValue.objects.select_related('property'), to_attr='meta_values_cached' - ) + ), + Prefetch( + 'variations', + queryset=ItemVariation.objects.prefetch_related( + Prefetch( + 'meta_values', + ItemVariationMetaValue.objects.select_related('property'), + to_attr='meta_values_cached' + ), + ), + ), ).select_related('category', 'tax_rule'): - m = i.meta_data vars = list(i.variations.all()) if vars: for v in vars: + m = v.meta_data row = [ i.pk, v.pk, @@ -160,6 +169,7 @@ class ItemDataExporter(ListExporter): yield row else: + m = i.meta_data row = [ i.pk, "", diff --git a/src/pretix/base/exporters/json.py b/src/pretix/base/exporters/json.py index 0eb825b977..6d0cd75243 100644 --- a/src/pretix/base/exporters/json.py +++ b/src/pretix/base/exporters/json.py @@ -36,9 +36,11 @@ import json from decimal import Decimal from django.core.serializers.json import DjangoJSONEncoder +from django.db.models import Prefetch from django.dispatch import receiver from ..exporter import BaseExporter +from ..models import ItemMetaValue, ItemVariation, ItemVariationMetaValue from ..signals import register_data_exporters @@ -106,9 +108,26 @@ class JSONExporter(BaseExporter): 'available_from': variation.available_from, 'available_until': variation.available_until, 'hide_without_voucher': variation.hide_without_voucher, + 'meta_data': variation.meta_data, } for variation in item.variations.all() ] - } for item in self.event.items.select_related('tax_rule').prefetch_related('variations') + } for item in self.event.items.select_related('tax_rule').prefetch_related( + Prefetch( + 'meta_values', + ItemMetaValue.objects.select_related('property'), + to_attr='meta_values_cached' + ), + Prefetch( + 'variations', + queryset=ItemVariation.objects.prefetch_related( + Prefetch( + 'meta_values', + ItemVariationMetaValue.objects.select_related('property'), + to_attr='meta_values_cached' + ), + ), + ), + ) ], 'questions': [ { diff --git a/src/pretix/base/migrations/0226_itemvariationmetavalue.py b/src/pretix/base/migrations/0226_itemvariationmetavalue.py new file mode 100644 index 0000000000..277159e80a --- /dev/null +++ b/src/pretix/base/migrations/0226_itemvariationmetavalue.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.16 on 2022-12-09 10:06 + +import django.db.models.deletion +from django.db import migrations, models + +import pretix.base.models.base + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0225_orderpayment_process_initiated'), + ] + + operations = [ + migrations.CreateModel( + name='ItemVariationMetaValue', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)), + ('value', models.TextField()), + ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variation_values', to='pretixbase.itemmetaproperty')), + ('variation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values', to='pretixbase.itemvariation')), + ], + options={ + 'unique_together': {('variation', 'property')}, + }, + bases=(models.Model, pretix.base.models.base.LoggingMixin), + ), + ] diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py index c1687ad049..626d8263a5 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -34,8 +34,8 @@ from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction from .invoices import Invoice, InvoiceLine, invoice_filename from .items import ( Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue, - ItemVariation, Question, QuestionOption, Quota, SubEventItem, - SubEventItemVariation, itempicture_upload_to, + ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota, + SubEventItem, SubEventItemVariation, itempicture_upload_to, ) from .log import LogEntry from .memberships import Membership, MembershipType diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 0f6793f9d1..663657ea3a 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -728,7 +728,7 @@ class Event(EventMixin, LoggedModel): from ..signals import event_copy_data from . import ( Discount, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, - Question, Quota, + ItemVariationMetaValue, Question, Quota, ) # Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin. @@ -804,12 +804,18 @@ class Event(EventMixin, LoggedModel): v.item = i v.save(force_insert=True) - for imv in ItemMetaValue.objects.filter(item__event=other).prefetch_related('item', 'property'): + for imv in ItemMetaValue.objects.filter(item__event=other): imv.pk = None - imv.property = item_meta_properties_map[imv.property.pk] + imv.property = item_meta_properties_map[imv.property_id] imv.item = item_map[imv.item.pk] imv.save(force_insert=True) + for imv in ItemVariationMetaValue.objects.filter(variation__item__event=other): + imv.pk = None + imv.property = item_meta_properties_map[imv.property_id] + imv.variation = variation_map[imv.variation_id] + imv.save(force_insert=True) + for ia in ItemAddOn.objects.filter(base_item__event=other).prefetch_related('base_item', 'addon_category'): ia.pk = None ia.base_item = item_map[ia.base_item.pk] diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index 6a4fb6386d..94c60dffcb 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -1008,6 +1008,16 @@ class ItemVariation(models.Model): return False return True + @property + def meta_data(self): + data = self.item.meta_data + if hasattr(self, 'meta_values_cached'): + data.update({v.property.name: v.value for v in self.meta_values_cached}) + else: + data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()}) + + return OrderedDict((k, v) for k, v in sorted(data.items(), key=lambda k: k[0])) + class ItemAddOn(models.Model): """ @@ -1787,8 +1797,21 @@ class ItemMetaValue(LoggedModel): class Meta: unique_together = ('item', 'property') - def delete(self, *args, **kwargs): - super().delete(*args, **kwargs) - def save(self, *args, **kwargs): - super().save(*args, **kwargs) +class ItemVariationMetaValue(LoggedModel): + """ + A meta-data value assigned to an item variation, overriding the value on the item. + + :param variation: The variation this metadata is valid for + :type variation: ItemVariation + :param property: The property this value belongs to + :type property: ItemMetaProperty + :param value: The actual value + :type value: str + """ + variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE, related_name='meta_values') + property = models.ForeignKey('ItemMetaProperty', on_delete=models.CASCADE, related_name='variation_values') + value = models.TextField() + + class Meta: + unique_together = ('variation', 'property') diff --git a/src/pretix/base/pdf.py b/src/pretix/base/pdf.py index 8eeffd8d39..b156e198fc 100644 --- a/src/pretix/base/pdf.py +++ b/src/pretix/base/pdf.py @@ -746,6 +746,8 @@ class Renderer: def replace(x): if x.group(1).startswith('itemmeta:'): + if op.variation_id: + return op.variation.meta_data.get(x.group(1)[9:]) or '' return op.item.meta_data.get(x.group(1)[9:]) or '' elif x.group(1).startswith('meta:'): return ev.meta_data.get(x.group(1)[5:]) or '' @@ -766,6 +768,8 @@ class Renderer: return re.sub(r'\{([a-zA-Z0-9:_]+)\}', replace, text) elif o['content'].startswith('itemmeta:'): + if op.variation_id: + return op.variation.meta_data.get(o['content'][9:]) or '' return op.item.meta_data.get(o['content'][9:]) or '' elif o['content'].startswith('meta:'): diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index 3dbaae8606..38157b4709 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -44,6 +44,7 @@ from django.core.files.uploadedfile import UploadedFile from django.db.models import Max from django.forms.formsets import DELETION_FIELD_NAME from django.urls import reverse +from django.utils.functional import cached_property from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.translation import ( @@ -436,12 +437,21 @@ class ItemCreateForm(I18nModelForm): v.pk = None v.item = instance v.save() + for mv in variation.meta_values.all(): + mv.pk = None + mv.variation = v + mv.save(force_insert=True) else: ItemVariation.objects.create( item=instance, value=__('Standard') ) if self.cleaned_data.get('copy_from'): + for mv in self.cleaned_data['copy_from'].meta_values.all(): + mv.pk = None + mv.item = instance + mv.save(force_insert=True) + for question in self.cleaned_data['copy_from'].questions.all(): question.items.add(instance) question.log_action('pretix.event.question.changed', user=self.user, data={ @@ -727,6 +737,31 @@ class ItemVariationForm(I18nModelForm): del self.fields['require_membership'] del self.fields['require_membership_types'] + self.meta_fields = [] + meta_defaults = {} + if self.instance.pk: + for mv in self.instance.meta_values.all(): + meta_defaults[mv.property_id] = mv.value + for p in self.meta_properties: + self.initial[f'meta_{p.name}'] = meta_defaults.get(p.pk) + self.fields[f'meta_{p.name}'] = forms.CharField( + label=p.name, + widget=forms.TextInput( + attrs={ + 'placeholder': _('Use value from product'), + 'data-typeahead-url': reverse('control:event.items.meta.typeahead', kwargs={ + 'organizer': self.event.organizer.slug, + 'event': self.event.slug + }) + '?' + urlencode({ + 'property': p.name, + }), + }, + ), + required=False, + + ) + self.meta_fields.append(f'meta_{p.name}') + class Meta: model = ItemVariation localized_fields = '__all__' @@ -757,6 +792,26 @@ class ItemVariationForm(I18nModelForm): }), } + def save(self, commit=True): + instance = super().save(commit) + self.meta_fields = [] + current_values = {v.property_id: v for v in instance.meta_values.all()} + for p in self.meta_properties: + if self.cleaned_data[f'meta_{p.name}']: + if p.pk in current_values: + current_values[p.pk].value = self.cleaned_data[f'meta_{p.name}'] + current_values[p.pk].save() + else: + instance.meta_values.create(property=p, value=self.cleaned_data[f'meta_{p.name}']) + elif p.pk in current_values: + current_values[p.pk].delete() + + @property + def meta_properties(self): + if not hasattr(self.event, '_cached_item_meta_properties'): + self.event._cached_item_meta_properties = self.event.item_meta_properties.all() + return self.event._cached_item_meta_properties + class ItemAddOnsFormSet(I18nFormSet): title = _('Add-ons') @@ -845,6 +900,7 @@ class ItemBundleFormSet(I18nFormSet): def _construct_form(self, i, **kwargs): kwargs['event'] = self.event kwargs['item'] = self.item + kwargs['item_qs'] = self.item_qs return super()._construct_form(i, **kwargs) @property @@ -856,12 +912,17 @@ class ItemBundleFormSet(I18nFormSet): empty_permitted=True, use_required_attribute=False, locales=self.locales, + item_qs=self.item_qs, item=self.item, event=self.event ) self.add_fields(form, None) return form + @cached_property + def item_qs(self): + return self.event.items.prefetch_related('variations').all() + def clean(self): super().clean() ivs = set() @@ -889,6 +950,7 @@ class ItemBundleForm(I18nModelForm): def __init__(self, *args, **kwargs): self.item = kwargs.pop('item') + self.item_qs = kwargs.pop('item_qs') super().__init__(*args, **kwargs) instance = kwargs.get('instance', None) initial = kwargs.get('initial', {}) @@ -906,7 +968,7 @@ class ItemBundleForm(I18nModelForm): super().__init__(*args, **kwargs) choices = [] - for i in self.event.items.prefetch_related('variations').all(): + for i in self.item_qs: pname = str(i) if not i.is_available(): pname += ' ({})'.format(_('inactive')) diff --git a/src/pretix/control/templates/pretixcontrol/item/include_variations.html b/src/pretix/control/templates/pretixcontrol/item/include_variations.html index 5632ac0194..2e49777bba 100644 --- a/src/pretix/control/templates/pretixcontrol/item/include_variations.html +++ b/src/pretix/control/templates/pretixcontrol/item/include_variations.html @@ -1,6 +1,7 @@ {% load i18n %} {% load bootstrap3 %} {% load formset_tags %} +{% load getitem %}