Allow to hide a product unless a specific quota is sold out (#1351)

* Allow to hide a product unless a specific quota is sold out

* Fix required property

* Add API property and copy between events
This commit is contained in:
Raphael Michel
2019-07-25 16:14:24 +02:00
committed by GitHub
parent 2dd75ea252
commit e83e8cdcc0
16 changed files with 250 additions and 8 deletions

View File

@@ -119,7 +119,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')
'show_quota_left', 'hidden_if_available')
read_only_fields = ('has_variations', 'picture')
def get_serializer_context(self):

View File

@@ -0,0 +1,21 @@
# Generated by Django 2.2.1 on 2019-07-24 15:48
import django.db.models.deletion
from django.db import migrations, models
import pretix.base.models.fields
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0128_auto_20190715_1510'),
]
operations = [
migrations.AddField(
model_name='item',
name='hidden_if_available',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='pretixbase.Quota'),
),
]

View File

@@ -516,6 +516,7 @@ class Event(EventMixin, LoggedModel):
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
items = list(q.items.all())
vars = list(q.variations.all())
oldid = q.pk
q.pk = None
q.event = self
q.cached_availability_state = None
@@ -529,6 +530,7 @@ class Event(EventMixin, LoggedModel):
q.items.add(item_map[i.pk])
for v in vars:
q.variations.add(variation_map[v.pk])
self.items.filter(hidden_if_available_id=oldid).update(hidden_if_available=q)
question_map = {}
for q in Question.objects.filter(event=other).prefetch_related('items', 'options'):

View File

@@ -334,6 +334,17 @@ class Item(LoggedModel):
null=True, blank=True,
help_text=_('This product will not be sold after the given date.')
)
hidden_if_available = models.ForeignKey(
'Quota',
null=True, blank=True,
on_delete=models.SET_NULL,
verbose_name=_("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.")
)
require_voucher = models.BooleanField(
verbose_name=_('This product can only be bought using a voucher.'),
default=False,

View File

@@ -402,6 +402,19 @@ 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(
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': _('Quota')
}
)
self.fields['hidden_if_available'].widget.choices = self.fields['hidden_if_available'].choices
self.fields['hidden_if_available'].required = False
class Meta:
model = Item
@@ -430,11 +443,13 @@ class ItemUpdateForm(I18nModelForm):
'generate_tickets',
'original_price',
'require_bundling',
'show_quota_left'
'show_quota_left',
'hidden_if_available',
]
field_classes = {
'available_from': SplitDateTimeField,
'available_until': SplitDateTimeField,
'hidden_if_available': SafeModelChoiceField,
}
widgets = {
'available_from': SplitDateTimePickerWidget(),

View File

@@ -31,6 +31,7 @@
{% bootstrap_field form.available_until layout="control" %}
{% bootstrap_field form.max_per_order layout="control" %}
{% bootstrap_field form.min_per_order layout="control" %}
{% bootstrap_field form.hidden_if_available layout="control" %}
{% bootstrap_field form.require_voucher layout="control" %}
{% bootstrap_field form.hide_without_voucher layout="control" %}
{% bootstrap_field form.require_bundling layout="control" %}

View File

@@ -181,6 +181,7 @@ urlpatterns = [
url(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
url(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'),
url(r'^quotas/(?P<quota>\d+)/$', item.QuotaView.as_view(), name='event.items.quotas.show'),
url(r'^quotas/select$', typeahead.quotas_select2, name='event.items.quotas.select2'),
url(r'^quotas/(?P<quota>\d+)/change$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'),
url(r'^quotas/(?P<quota>\d+)/delete$', item.QuotaDelete.as_view(),
name='event.items.quotas.delete'),

View File

@@ -309,6 +309,53 @@ def subevent_select2(request, **kwargs):
return JsonResponse(doc)
@event_permission_required(None)
def quotas_select2(request, **kwargs):
query = request.GET.get('query', '')
try:
page = int(request.GET.get('page', '1'))
except ValueError:
page = 1
qf = Q(name__icontains=query) | Q(subevent__name__icontains=i18ncomp(query))
tz = request.event.timezone
dt = None
for f in get_format('DATE_INPUT_FORMATS'):
try:
dt = datetime.strptime(query, f)
break
except (ValueError, TypeError):
continue
if dt and request.event.has_subevents:
dt_start = make_aware(datetime.combine(dt.date(), time(hour=0, minute=0, second=0)), tz)
dt_end = make_aware(datetime.combine(dt.date(), time(hour=23, minute=59, second=59)), tz)
qf |= Q(subevent__date_from__gte=dt_start) & Q(subevent__date_from__lte=dt_end)
qs = request.event.quotas.filter(
qf
).order_by('-subevent__date_from', 'name')
total = qs.count()
pagesize = 20
offset = (page - 1) * pagesize
doc = {
'results': [
{
'id': q.pk,
'name': str(q.name),
'text': q.name
}
for q in qs[offset:offset + pagesize]
],
'pagination': {
"more": total >= (offset + pagesize)
}
}
return JsonResponse(doc)
@event_permission_required(None)
def checkinlist_select2(request, **kwargs):
query = request.GET.get('query', '')

View File

@@ -254,6 +254,11 @@ class AddOnsForm(forms.Form):
self.vars_cache = {}
for i in items:
if i.hidden_if_available:
q = i.hidden_if_available.availability(_cache=quota_cache)
if q[0] == Quota.AVAILABILITY_OK:
continue
if i.has_variations:
choices = [('', _('no selection'), '')]
for v in i.available_variations:

View File

@@ -52,6 +52,7 @@ def item_group_by_category(items):
def get_grouped_items(event, subevent=None, voucher=None, channel='web', require_seat=0):
items = event.items.using(settings.DATABASE_REPLICA).filter_available(channel=channel, voucher=voucher).select_related(
'category', 'tax_rule', # for re-grouping
'hidden_if_available',
).prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',
@@ -119,6 +120,12 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
max_per_order = item.max_per_order or int(event.settings.max_items_per_order)
if item.hidden_if_available:
q = item.hidden_if_available.availability(_cache=quota_cache)
if q[0] == Quota.AVAILABILITY_OK:
item._remove = True
continue
if not item.has_variations:
item._remove = False
if not bool(item._subevent_quotas):