Allow to hide products that require membership (#2240)

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2021-10-07 10:11:31 +02:00
committed by GitHub
parent f459f1f12d
commit 0f47bff5cd
18 changed files with 193 additions and 13 deletions

View File

@@ -59,7 +59,7 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price',
'require_membership', 'require_membership_types', 'available_from', 'available_until',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
def __init__(self, *args, **kwargs):
@@ -75,7 +75,7 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price',
'require_membership', 'require_membership_types', 'available_from', 'available_until',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
def __init__(self, *args, **kwargs):
@@ -175,7 +175,7 @@ class ItemSerializer(I18nAwareModelSerializer):
'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',
'require_membership', 'require_membership_types', 'grant_membership_type',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
'grant_membership_duration_like_event', 'grant_membership_duration_days',
'grant_membership_duration_months')
read_only_fields = ('has_variations',)

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.4 on 2021-10-05 10:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0198_invoice_sent_to_customer'),
]
operations = [
migrations.AddField(
model_name='item',
name='require_membership_hidden',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='itemvariation',
name='require_membership_hidden',
field=models.BooleanField(default=False),
),
]

View File

@@ -25,6 +25,7 @@ from django.contrib.auth.hashers import (
check_password, is_password_usable, make_password,
)
from django.db import models
from django.db.models import F, Q
from django.utils.crypto import get_random_string, salted_hmac
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled
@@ -183,6 +184,12 @@ class Customer(LoggedModel):
def stored_addresses(self):
return self.invoice_addresses(manager='profiles')
def usable_memberships(self, for_event, testmode=False):
return self.memberships.active(for_event).with_usages().filter(
Q(membership_type__max_usages__isnull=True) | Q(usages__lt=F('membership_type__max_usages')),
testmode=testmode,
)
class AttendeeProfile(models.Model):
customer = models.ForeignKey(

View File

@@ -523,6 +523,12 @@ class Item(LoggedModel):
verbose_name=_('Allowed membership types'),
blank=True,
)
require_membership_hidden = models.BooleanField(
verbose_name=_('Hide without a valid membership'),
help_text=_('Do not show this unless the customer is logged in and has a valid membership. Be aware that '
'this means it will never be visible in the widget.'),
default=False,
)
grant_membership_type = models.ForeignKey(
'MembershipType',
null=True, blank=True,
@@ -802,6 +808,12 @@ class ItemVariation(models.Model):
verbose_name=_('Membership types'),
blank=True,
)
require_membership_hidden = models.BooleanField(
verbose_name=_('Hide without a valid membership'),
help_text=_('Do not show this unless the customer is logged in and has a valid membership. Be aware that '
'this means it will never be visible in the widget.'),
default=False,
)
available_from = models.DateTimeField(
verbose_name=_("Available from"),
null=True, blank=True,

View File

@@ -95,6 +95,7 @@ class MembershipQuerySet(models.QuerySet):
def active(self, ev):
return self.filter(
canceled=False,
date_start__lte=ev.date_from,
date_end__gte=ev.date_from
)
@@ -175,7 +176,7 @@ class Membership(models.Model):
else:
dt = now()
return dt >= self.date_start and dt <= self.date_end
return not self.canceled and dt >= self.date_start and dt <= self.date_end
def allow_delete(self):
return self.testmode and not self.orderposition_set.exists()

View File

@@ -607,6 +607,7 @@ class ItemUpdateForm(I18nModelForm):
'issue_giftcard',
'require_membership',
'require_membership_types',
'require_membership_hidden',
'grant_membership_type',
'grant_membership_duration_like_event',
'grant_membership_duration_days',
@@ -713,6 +714,7 @@ class ItemVariationForm(I18nModelForm):
'original_price',
'description',
'require_membership',
'require_membership_hidden',
'require_membership_types',
'available_from',
'available_until',

View File

@@ -77,6 +77,7 @@
{% bootstrap_field form.require_membership layout="control" %}
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
{% bootstrap_field form.require_membership_types layout="control" %}
{% bootstrap_field form.require_membership_hidden layout="control" %}
</div>
{% endif %}
</div>
@@ -147,6 +148,7 @@
{% bootstrap_field formset.empty_form.require_membership layout="control" %}
<div data-display-dependency="#{{ formset.empty_form.require_membership.id_for_label }}">
{% bootstrap_field formset.empty_form.require_membership_types layout="control" %}
{% bootstrap_field formset.empty_form.require_membership_hidden layout="control" %}
</div>
{% endif %}
</div>

View File

@@ -105,6 +105,7 @@
{% bootstrap_field form.require_membership layout="control" %}
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
{% bootstrap_field form.require_membership_types layout="control" %}
{% bootstrap_field form.require_membership_hidden layout="control" %}
</div>
{% endif %}
{% bootstrap_field form.allow_cancel layout="control" %}

View File

@@ -499,7 +499,14 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
channel=self.request.sales_channel.identifier,
base_qs=iao.addon_category.items,
allow_addons=True,
quota_cache=quota_cache
quota_cache=quota_cache,
memberships=(
self.request.customer.usable_memberships(
for_event=cartpos.subevent or self.request.event,
testmode=self.request.event.testmode
)
if getattr(self.request, 'customer', None) else None
),
)
item_cache[ckey] = items
else:

View File

@@ -43,13 +43,21 @@ class WaitingListForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event')
self.channel = kwargs.pop('channel')
customer = kwargs.pop('customer')
super().__init__(*args, **kwargs)
choices = [
('', '')
]
items, display_add_to_cart = get_grouped_items(
self.event, self.instance.subevent, require_seat=None
self.event, self.instance.subevent, require_seat=None,
memberships=(
self.request.customer.usable_memberships(
for_event=self.instance.subevent or self.event,
testmode=self.request.event.testmode
)
if customer else None
),
)
for i in items:
if not i.allow_waitinglist:

View File

@@ -523,8 +523,18 @@ class RedeemView(NoSearchIndexViewMixin, EventViewMixin, CartMixin, TemplateView
context['max_times'] = self.voucher.max_usages - self.voucher.redeemed
# Fetch all items
items, display_add_to_cart = get_grouped_items(self.request.event, self.subevent,
voucher=self.voucher, channel=self.request.sales_channel.identifier)
items, display_add_to_cart = get_grouped_items(
self.request.event,
self.subevent,
voucher=self.voucher,
channel=self.request.sales_channel.identifier,
memberships=(
self.request.customer.usable_memberships(
for_event=self.subevent or self.request.event,
testmode=self.request.event.testmode
) if getattr(self.request, 'customer', None) else None
),
)
# Calculate how many options the user still has. If there is only one option, we can
# check the box right away ;)

View File

@@ -100,7 +100,7 @@ def item_group_by_category(items):
def get_grouped_items(event, subevent=None, voucher=None, channel='web', require_seat=0, base_qs=None, allow_addons=False,
quota_cache=None, filter_items=None, filter_categories=None):
quota_cache=None, filter_items=None, filter_categories=None, memberships=None):
base_qs_set = base_qs is not None
base_qs = base_qs if base_qs is not None else event.items
@@ -120,10 +120,16 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
if not voucher or not voucher.show_hidden_items:
variation_q &= Q(hide_without_voucher=False)
if memberships is not None:
prefetch_membership_types = ['require_membership_types']
else:
prefetch_membership_types = []
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)),
@@ -160,6 +166,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
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))
@@ -240,6 +247,11 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
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
continue
item.description = str(item.description)
for recv, resp in item_description.send(sender=event, item=item, variation=None):
if resp:
@@ -290,6 +302,11 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
display_add_to_cart = display_add_to_cart or item.order_max > 0
else:
for var in item.available_variations:
if var.require_membership and var.require_membership_hidden:
if not memberships or not any([m.membership_type in var.require_membership_types.all() for m in memberships]):
var._remove = True
continue
var.description = str(var.description)
for recv, resp in item_description.send(sender=event, item=item, variation=var):
if resp:
@@ -338,7 +355,7 @@ def get_grouped_items(event, subevent=None, voucher=None, channel='web', require
item.available_variations = [
v for v in item.available_variations if v._subevent_quotas and (
not voucher or not voucher.quota_id or v in restrict_vars
)
) and not getattr(v, '_remove', False)
]
if event.settings.hide_sold_out:
@@ -439,7 +456,13 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
filter_items=self.request.GET.getlist('item'),
filter_categories=self.request.GET.getlist('category'),
require_seat=None,
channel=self.request.sales_channel.identifier
channel=self.request.sales_channel.identifier,
memberships=(
self.request.customer.usable_memberships(
for_event=self.subevent or self.request.event,
testmode=self.request.event.testmode
) if getattr(self.request, 'customer', None) else None
),
)
context['waitinglist_seated'] = False

View File

@@ -54,6 +54,7 @@ class WaitingView(EventViewMixin, FormView):
subevent=self.subevent
)
kwargs['channel'] = self.request.sales_channel.identifier
kwargs['customer'] = getattr(self.request, 'customer', None)
kwargs.setdefault('initial', {})
if 'var' in self.request.GET:
kwargs['initial']['itemvar'] = f'{self.request.GET.get("item")}-{self.request.GET.get("var")}'

View File

@@ -226,8 +226,17 @@ class WidgetAPIProductList(EventListMixin, View):
qs = qs.filter(category__pk__in=self.request.GET.get('categories').split(","))
items, display_add_to_cart = get_grouped_items(
self.request.event, subevent=self.subevent, voucher=self.voucher, channel=self.request.sales_channel.identifier,
base_qs=qs
self.request.event,
subevent=self.subevent,
voucher=self.voucher,
channel=self.request.sales_channel.identifier,
base_qs=qs,
memberships=(
self.request.customer.usable_memberships(
for_event=self.subevent or self.request.event,
testmode=self.request.event.testmode
) if getattr(self.request, 'customer', None) else None
),
)
grps = []