Allow variations to override item meta data (#2965)

This commit is contained in:
Raphael Michel
2022-12-12 12:06:09 +01:00
committed by GitHub
parent 5f899ed5c5
commit 3d9679a144
22 changed files with 440 additions and 55 deletions

View File

@@ -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'))

View File

@@ -1,6 +1,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% load getitem %}
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" id="item_variations">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
@@ -29,16 +30,20 @@
{% endif %}
</div>
<div class="col-md-2 col-xs-6">
<span class="fa fa-clock-o fa-fw text-muted variation-timeframe variation-icon-hidden" data-toggle="tooltip" title="{% trans "Only available in a limited timeframe" %}"></span>
<span class="fa fa-tags fa-fw text-muted variation-voucher variation-icon-hidden" data-toggle="tooltip"
title="{% trans "Only visible with a voucher" %}"></span>
<span class="fa fa-id-badge fa-fw text-muted variation-membership variation-icon-hidden" data-toggle="tooltip"
title="{% trans "Require a valid membership" %}"></span>
<span class="fa fa-clock-o fa-fw text-muted variation-timeframe variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Only available in a limited timeframe" %}"></span>
<span class="fa fa-tags fa-fw text-muted variation-voucher variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Only visible with a voucher" %}"></span>
<span class="fa fa-id-badge fa-fw text-muted variation-membership variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Require a valid membership" %}"></span>
</div>
<div class="col-md-2 col-xs-6">
{% for k, c in sales_channels.items %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ k }} variation-icon-hidden"
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
{% endfor %}
</div>
<div class="col-md-1 col-xs-6 text-right flip variation-price">
@@ -69,6 +74,27 @@
{% bootstrap_field form.default_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field form.original_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field form.description layout="control" %}
{% if form.meta_fields %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
<div class="col-md-9">
{% for fname in form.meta_fields %}
{% with form|getitem:fname as field %}
<div class="row">
<div class="col-md-4">
<label for="{{ field.id_for_label }}">
{{ field.label }}
</label>
</div>
<div class="col-md-8">
{% bootstrap_field field layout="inline" %}
</div>
</div>
{% endwith %}
{% endfor %}
</div>
</div>
{% endif %}
{% bootstrap_field form.available_from layout="control" %}
{% bootstrap_field form.available_until layout="control" %}
{% bootstrap_field form.sales_channels layout="control" %}
@@ -110,16 +136,20 @@
{% endif %}
</div>
<div class="col-md-2 col-xs-6">
<span class="fa fa-clock-o fa-fw text-muted variation-timeframe variation-icon-hidden" data-toggle="tooltip" title="{% trans "Only available in a limited timeframe" %}"></span>
<span class="fa fa-tags fa-fw text-muted variation-voucher variation-icon-hidden" data-toggle="tooltip"
title="{% trans "Only visible with a voucher" %}"></span>
<span class="fa fa-id-badge fa-fw text-muted variation-membership variation-icon-hidden" data-toggle="tooltip"
title="{% trans "Require a valid membership" %}"></span>
<span class="fa fa-clock-o fa-fw text-muted variation-timeframe variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Only available in a limited timeframe" %}"></span>
<span class="fa fa-tags fa-fw text-muted variation-voucher variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Only visible with a voucher" %}"></span>
<span class="fa fa-id-badge fa-fw text-muted variation-membership variation-icon-hidden"
data-toggle="tooltip"
title="{% trans "Require a valid membership" %}"></span>
</div>
<div class="col-md-2 col-xs-6">
{% for k, c in sales_channels.items %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted variation-channel-{{ k }} variation-icon-hidden"
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
data-toggle="tooltip" title="{% trans c.verbose_name %}"></span>
{% endfor %}
</div>
<div class="col-md-1 col-xs-6 text-right flip variation-price">
@@ -141,6 +171,27 @@
{% bootstrap_field formset.empty_form.default_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field formset.empty_form.original_price addon_after=request.event.currency layout="control" %}
{% bootstrap_field formset.empty_form.description layout="control" %}
{% if formset.empty_form.meta_fields %}
<div class="form-group metadata-group">
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
<div class="col-md-9">
{% for fname in formset.empty_form.meta_fields %}
{% with formset.empty_form|getitem:fname as field %}
<div class="row">
<div class="col-md-4">
<label for="{{ field.id_for_label }}">
{{ field.label }}
</label>
</div>
<div class="col-md-8">
{% bootstrap_field field layout="inline" %}
</div>
</div>
{% endwith %}
{% endfor %}
</div>
</div>
{% endif %}
{% bootstrap_field formset.empty_form.available_from layout="control" %}
{% bootstrap_field formset.empty_form.available_until layout="control" %}
{% bootstrap_field formset.empty_form.sales_channels layout="control" %}

View File

@@ -1426,7 +1426,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
can_order=True, can_delete=True, extra=0
)(
self.request.POST if self.request.method == "POST" else None,
queryset=ItemVariation.objects.filter(item=self.get_object()),
queryset=ItemVariation.objects.filter(item=self.get_object()).prefetch_related('meta_values', 'require_membership_types'),
event=self.request.event, prefix="variations"
)),
('addons', inlineformset_factory(

View File

@@ -48,7 +48,7 @@ from django.utils.translation import gettext as _, pgettext
from pretix.base.models import (
EventMetaProperty, EventMetaValue, ItemMetaProperty, ItemMetaValue,
ItemVariation, Order, Organizer, User, Voucher,
ItemVariation, ItemVariationMetaValue, Order, Organizer, User, Voucher,
)
from pretix.control.forms.event import EventWizardCopyForm
from pretix.control.permissions import (
@@ -747,6 +747,10 @@ def item_meta_values(request, organizer, event):
value__icontains=q,
property__name=propname
)
var_matches = ItemVariationMetaValue.objects.filter(
value__icontains=q,
property__name=propname
)
defaults = ItemMetaProperty.objects.filter(
name=propname,
default__icontains=q
@@ -758,6 +762,7 @@ def item_meta_values(request, organizer, event):
defaults = defaults.filter(event__organizer_id=organizer.pk)
matches = matches.filter(item__event__organizer_id=organizer.pk)
var_matches = var_matches.filter(variation__item__event__organizer_id=organizer.pk)
all_access = (
request.user.has_active_staff_session(request.session.session_key)
or request.user.teams.filter(all_events=True, organizer=organizer, can_change_items=True).exists()
@@ -773,10 +778,19 @@ def item_meta_values(request, organizer, event):
'limit_events__id', flat=True
)
)
var_matches = matches.filter(
variation__item__event__id__in=request.user.teams.filter(can_change_items=True).values_list(
'limit_events__id', flat=True
)
)
return JsonResponse({
'results': [
{'name': v, 'id': v}
for v in sorted(set(defaults.values_list('default', flat=True)[:10]) | set(matches.values_list('value', flat=True)[:10]))
for v in sorted(
set(defaults.values_list('default', flat=True)[:10]) |
set(matches.values_list('value', flat=True)[:10]) |
set(var_matches.values_list('value', flat=True)[:10])
)
]
})