Replace Item.hidden_if_available with relationship to other Item (#3686)

* draft

* Implementation that is closer to old one

* Fix tests

* Add tests

* Update src/pretix/control/forms/item.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Review notes

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2023-11-06 13:26:32 +01:00
committed by GitHub
parent 3d68c83907
commit 3af2342d7b
15 changed files with 235 additions and 54 deletions

View File

@@ -239,7 +239,8 @@ 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', 'meta_data',
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'allow_waitinglist',
'issue_giftcard', 'meta_data',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
'grant_membership_duration_like_event', 'grant_membership_duration_days',
'grant_membership_duration_months', 'validity_mode', 'validity_fixed_from', 'validity_fixed_until',

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.2.4 on 2023-10-30 11:50
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0248_item_free_price_suggestion"),
]
operations = [
migrations.AddField(
model_name="item",
name="hidden_if_item_available",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="pretixbase.item",
),
),
]

View File

@@ -857,6 +857,10 @@ class Event(EventMixin, LoggedModel):
v.item = i
v.save(force_insert=True)
for i in self.items.filter(hidden_if_item_available__isnull=False):
i.hidden_if_item_available = item_map[i.hidden_if_item_available_id]
i.save()
for imv in ItemMetaValue.objects.filter(item__event=other):
imv.pk = None
imv.property = item_meta_properties_map[imv.property_id]

View File

@@ -494,13 +494,24 @@ class Item(LoggedModel):
'Quota',
null=True, blank=True,
on_delete=models.SET_NULL,
verbose_name=_("Only show after sellout of"),
verbose_name=pgettext_lazy("hidden_if_available_legacy", "Only show after sellout of"),
help_text=_("If you select a quota here, this product will only be shown when that quota is "
"unavailable. If combined with the option to hide sold-out products, this allows you to "
"swap out products for more expensive ones once they are sold out. There might be a short period "
"in which both products are visible while all tickets in the referenced quota are reserved, "
"but not yet sold.")
)
hidden_if_item_available = models.ForeignKey(
'Item',
null=True, blank=True,
on_delete=models.SET_NULL,
verbose_name=_("Only show after sellout of"),
help_text=_("If you select a product here, this product will only be shown when that product is "
"sold out. If combined with the option to hide sold-out products, this allows you to "
"swap out products for more expensive ones once the cheaper option is sold out. There might "
"be a short period in which both products are visible while all tickets of the referenced "
"product are reserved, but not yet sold.")
)
require_voucher = models.BooleanField(
verbose_name=_('This product can only be bought using a voucher.'),
default=False,

View File

@@ -45,7 +45,7 @@ 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.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import (
gettext as __, gettext_lazy as _, pgettext_lazy,
@@ -387,6 +387,7 @@ class ItemCreateForm(I18nModelForm):
'allow_waitinglist',
'show_quota_left',
'hidden_if_available',
'hidden_if_item_available',
'require_bundling',
'require_membership',
'grant_membership_type',
@@ -550,19 +551,43 @@ class ItemUpdateForm(I18nModelForm):
widget=forms.CheckboxSelectMultiple
)
change_decimal_field(self.fields['default_price'], self.event.currency)
self.fields['hidden_if_available'].queryset = self.event.quotas.all()
self.fields['hidden_if_available'].widget = Select2(
if self.instance.hidden_if_available_id:
self.fields['hidden_if_available'].queryset = self.event.quotas.all()
self.fields['hidden_if_available'].help_text = format_html(
"<strong>{}</strong> {}",
_("This option is deprecated. For new products, use the newer option below that refers to another "
"product instead of a quota."),
self.fields['hidden_if_available'].help_text
)
self.fields['hidden_if_available'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:event.items.quotas.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': _('Shown independently of other products')
}
)
self.fields['hidden_if_available'].widget.choices = self.fields['hidden_if_available'].choices
self.fields['hidden_if_available'].required = False
else:
del self.fields['hidden_if_available']
self.fields['hidden_if_item_available'].queryset = self.event.items.exclude(id=self.instance.id)
self.fields['hidden_if_item_available'].widget = Select2(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:event.items.quotas.select2', kwargs={
'data-select2-url': reverse('control:event.items.select2', kwargs={
'event': self.event.slug,
'organizer': self.event.organizer.slug,
}),
'data-placeholder': _('Shown independently of other products')
}
)
self.fields['hidden_if_available'].widget.choices = self.fields['hidden_if_available'].choices
self.fields['hidden_if_available'].required = False
self.fields['hidden_if_item_available'].widget.choices = self.fields['hidden_if_item_available'].choices
self.fields['hidden_if_item_available'].required = False
self.fields['category'].queryset = self.instance.event.categories.all()
self.fields['category'].widget = Select2(
@@ -683,6 +708,7 @@ class ItemUpdateForm(I18nModelForm):
'require_bundling',
'show_quota_left',
'hidden_if_available',
'hidden_if_item_available',
'issue_giftcard',
'require_membership',
'require_membership_types',
@@ -709,6 +735,7 @@ class ItemUpdateForm(I18nModelForm):
'validity_fixed_from': SplitDateTimeField,
'validity_fixed_until': SplitDateTimeField,
'hidden_if_available': SafeModelChoiceField,
'hidden_if_item_available': SafeModelChoiceField,
'grant_membership_type': SafeModelChoiceField,
'require_membership_types': SafeModelMultipleChoiceField,
}

View File

@@ -32,6 +32,14 @@
{% endblocktrans %}
</div>
{% endif %}
{% if not request.event.has_subevents and object.hidden_if_item_available and object.hidden_if_item_available.check_quotas.0 == 100 %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
This product is currently not being shown since you configured below that it should only be visible
if a certain other product is already sold out.
{% endblocktrans %}
</div>
{% endif %}
{% block inside %}
{% endblock %}

View File

@@ -169,7 +169,10 @@
{% endif %}
{% bootstrap_field form.allow_cancel layout="control" %}
{% bootstrap_field form.allow_waitinglist layout="control" %}
{% bootstrap_field form.hidden_if_available layout="control" %}
{% if form.hidden_if_available %}
{% bootstrap_field form.hidden_if_available layout="control" %}
{% endif %}
{% bootstrap_field form.hidden_if_item_available layout="control" %}
</fieldset>
{% for v in formsets.values %}
<fieldset>

View File

@@ -4255,7 +4255,7 @@ msgstr "Produktbild"
#: pretix/base/models/items.py:491
msgid "Only show after sellout of"
msgstr "Nicht anzeigen, wenn anderes Kontingent verfügbar"
msgstr "Nicht anzeigen, wenn anderes Produkt verfügbar"
#: pretix/base/models/items.py:492
msgid ""

View File

@@ -4252,7 +4252,7 @@ msgstr "Produktbild"
#: pretix/base/models/items.py:491
msgid "Only show after sellout of"
msgstr "Nicht anzeigen, wenn anderes Kontingent verfügbar"
msgstr "Nicht anzeigen, wenn anderes Produkt verfügbar"
#: pretix/base/models/items.py:492
msgid ""

View File

@@ -127,52 +127,77 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
else:
prefetch_membership_types = []
prefetch_var = Prefetch(
'variations',
to_attr='available_variations',
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
subevent_disabled=Exists(
SubEventItemVariation.objects.filter(
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
variation_id=OuterRef('pk'),
subevent=subevent,
)
),
).filter(
variation_q,
active=True,
sales_channels__contains=channel,
quotas__isnull=False,
subevent_disabled=False
).prefetch_related(
*prefetch_membership_types,
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
subevent=subevent))
).distinct()
)
prefetch_quotas = Prefetch(
'quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent)
)
prefetch_bundles = Prefetch(
'bundles',
queryset=ItemBundle.objects.using(settings.DATABASE_REPLICA).prefetch_related(
Prefetch('bundled_item',
queryset=event.items.using(settings.DATABASE_REPLICA).select_related(
'tax_rule').prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
subevent=subevent)),
)),
Prefetch('bundled_variation',
queryset=ItemVariation.objects.using(
settings.DATABASE_REPLICA
).select_related('item', 'item__tax_rule').filter(item__event=event).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(
subevent=subevent)),
)),
)
)
items = base_qs.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher, allow_addons=allow_addons).select_related(
'category', 'tax_rule', # for re-grouping
'hidden_if_available',
).prefetch_related(
*prefetch_membership_types,
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent)),
Prefetch('bundles',
queryset=ItemBundle.objects.using(settings.DATABASE_REPLICA).prefetch_related(
Prefetch('bundled_item',
queryset=event.items.using(settings.DATABASE_REPLICA).select_related('tax_rule').prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent)),
)),
Prefetch('bundled_variation',
queryset=ItemVariation.objects.using(
settings.DATABASE_REPLICA
).select_related('item', 'item__tax_rule').filter(item__event=event).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent)),
)),
)),
Prefetch('variations', to_attr='available_variations',
queryset=ItemVariation.objects.using(settings.DATABASE_REPLICA).annotate(
subevent_disabled=Exists(
SubEventItemVariation.objects.filter(
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
variation_id=OuterRef('pk'),
subevent=subevent,
)
),
).filter(
variation_q,
active=True,
sales_channels__contains=channel,
quotas__isnull=False,
subevent_disabled=False
).prefetch_related(
*prefetch_membership_types,
Prefetch('quotas',
to_attr='_subevent_quotas',
queryset=event.quotas.using(settings.DATABASE_REPLICA).filter(subevent=subevent))
).distinct()),
Prefetch(
'hidden_if_item_available',
queryset=event.items.annotate(
has_variations=Count('variations'),
).prefetch_related(
prefetch_var,
prefetch_quotas,
prefetch_bundles,
)
),
prefetch_quotas,
prefetch_var,
prefetch_bundles,
).annotate(
quotac=Count('quotas'),
has_variations=Count('variations'),
@@ -256,6 +281,19 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
item._remove = True
continue
if item.hidden_if_item_available:
if item.hidden_if_item_available.has_variations:
dependency_available = any(
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
for var in item.hidden_if_item_available.available_variations
)
else:
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
dependency_available = q[0] == Quota.AVAILABILITY_OK
if dependency_available:
item._remove = True
continue
if item.require_membership and item.require_membership_hidden:
if not memberships or not any([m.membership_type in item.require_membership_types.all() for m in memberships]):
item._remove = True