Add option to limit events to specific sales channels (#1867)

This commit is contained in:
Martin Gross
2020-12-03 17:10:54 +01:00
committed by GitHub
parent 55d8639ecc
commit b3e95f54dd
17 changed files with 185 additions and 23 deletions

View File

@@ -124,7 +124,8 @@ class EventSerializer(I18nAwareModelSerializer):
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys')
'plugins', 'seat_category_mapping', 'timezone', 'item_meta_properties', 'valid_keys',
'sales_channels')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -28,6 +28,7 @@ with scopes_disabled():
is_past = django_filters.rest_framework.BooleanFilter(method='is_past_qs')
is_future = django_filters.rest_framework.BooleanFilter(method='is_future_qs')
ends_after = django_filters.rest_framework.IsoDateTimeFilter(method='ends_after_qs')
sales_channel = django_filters.rest_framework.CharFilter(method='sales_channel_qs')
class Meta:
model = Event
@@ -69,6 +70,9 @@ with scopes_disabled():
else:
return queryset.exclude(expr)
def sales_channel_qs(self, queryset, name, value):
return queryset.filter(sales_channels__contains=value)
class EventViewSet(viewsets.ModelViewSet):
serializer_class = EventSerializer

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.0.9 on 2020-12-02 12:37
from django.db import migrations
import pretix.base.models.fields
from pretix.base.channels import get_all_sales_channels
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0171_auto_20201126_1635'),
]
operations = [
migrations.AddField(
model_name='event',
name='sales_channels',
field=pretix.base.models.fields.MultiStringField(default=list(get_all_sales_channels().keys())),
),
]

View File

@@ -23,6 +23,7 @@ from django_scopes import ScopedManager, scopes_disabled
from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.validators import EventSlugBanlistValidator
from pretix.helpers.database import GroupConcat
@@ -331,6 +332,8 @@ class Event(EventMixin, LoggedModel):
:type plugins: str
:param has_subevents: Enable event series functionality
:type has_subevents: bool
:param sales_channels: A list of sales channel identifiers, that this event is available for sale on
:type sales_channels: list
"""
settings_namespace = 'event'
@@ -409,7 +412,11 @@ class Event(EventMixin, LoggedModel):
)
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
related_name='events')
sales_channels = MultiStringField(
verbose_name=_('Restrict to specific sales channels'),
help_text=_('Only sell tickets for this event on the following sales channels.'),
default=['web'],
)
objects = ScopedManager(organizer='organizer')
class Meta:

View File

@@ -5,7 +5,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.db.models import Q
from django.forms import formset_factory
from django.forms import CheckboxSelectMultiple, formset_factory
from django.urls import reverse
from django.utils.html import escape
from django.utils.safestring import mark_safe
@@ -311,6 +311,16 @@ class EventUpdateForm(I18nModelForm):
required=False,
help_text=_('You need to configure the custom domain in the webserver beforehand.')
)
self.fields['sales_channels'] = forms.MultipleChoiceField(
label=self.fields['sales_channels'].label,
help_text=self.fields['sales_channels'].help_text,
required=self.fields['sales_channels'].required,
initial=self.fields['sales_channels'].initial,
choices=(
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
),
widget=forms.CheckboxSelectMultiple
)
def clean_domain(self):
d = self.cleaned_data['domain']
@@ -367,6 +377,7 @@ class EventUpdateForm(I18nModelForm):
'location',
'geo_lat',
'geo_lon',
'sales_channels'
]
field_classes = {
'date_from': SplitDateTimeField,
@@ -381,6 +392,7 @@ class EventUpdateForm(I18nModelForm):
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
'sales_channels': CheckboxSelectMultiple()
}

View File

@@ -219,6 +219,7 @@
{% if sform.event_list_type %}
{% bootstrap_field sform.event_list_type layout="control" %}
{% endif %}
{% bootstrap_field form.sales_channels layout="control" %}
</fieldset>
<fieldset>
<legend>{% trans "Cart" %}</legend>

View File

@@ -2,6 +2,7 @@ from django.template.response import TemplateResponse
from django.urls import resolve
from django_scopes import scope
from pretix.base.channels import WebshopSalesChannel
from pretix.presale.signals import process_response
from .utils import _detect_event
@@ -20,6 +21,11 @@ class EventMiddleware:
def __call__(self, request):
url = resolve(request.path_info)
request._namespace = url.namespace
if not hasattr(request, 'sales_channel'):
# The environ lookup is only relevant during unit testing
request.sales_channel = request.environ.get('PRETIX_SALES_CHANNEL', WebshopSalesChannel())
if url.namespace != 'presale':
return self.get_response(request)

View File

@@ -11,7 +11,6 @@ from django.urls import resolve
from django.utils.translation import gettext_lazy as _
from django_scopes import scope
from pretix.base.channels import WebshopSalesChannel
from pretix.base.middleware import LocaleMiddleware
from pretix.base.models import Event, Organizer
from pretix.multidomain.urlreverse import (
@@ -32,6 +31,7 @@ def _detect_event(request, require_live=True, require_plugin=None):
db = settings.DATABASE_REPLICA
url = resolve(request.path_info)
try:
if hasattr(request, 'event_domain'):
# We are on an event's custom domain
@@ -127,9 +127,6 @@ def _detect_event(request, require_live=True, require_plugin=None):
if require_plugin not in request.event.get_plugins() and not is_core:
raise Http404(_('This feature is not enabled.'))
if not hasattr(request, 'sales_channel'):
# The environ lookup is only relevant during unit testing
request.sales_channel = request.environ.get('PRETIX_SALES_CHANNEL', WebshopSalesChannel())
for receiver, response in process_request.send(request.event, request=request):
if response:
return response

View File

@@ -419,6 +419,9 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
return u
def post(self, request, *args, **kwargs):
if request.sales_channel.identifier not in request.event.sales_channels:
raise Http404(_('Tickets for this event cannot be purchased on this sales channel.'))
cart_id = get_or_create_cart_id(self.request)
if "widget_data" in request.POST:
try:

View File

@@ -348,6 +348,9 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
r._csp_ignore = True
return r
if request.sales_channel.identifier not in request.event.sales_channels:
raise Http404(_('Tickets for this event cannot be purchased on this sales channel.'))
if request.event.has_subevents:
if 'subevent' in kwargs:
self.subevent = request.event.subevents.using(settings.DATABASE_REPLICA).filter(pk=kwargs['subevent'], active=True).first()

View File

@@ -101,6 +101,7 @@ class EventListMixin:
def _get_event_queryset(self):
query = Q(is_public=True) & Q(live=True)
qs = self.request.organizer.events.using(settings.DATABASE_REPLICA).filter(query)
qs = qs.filter(sales_channels__contains=self.request.sales_channel.identifier)
qs = qs.annotate(
min_from=Min('subevents__date_from'),
min_to=Min('subevents__date_to'),
@@ -487,11 +488,16 @@ class CalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
def _events_by_day(self, before, after):
ebd = defaultdict(list)
timezones = set()
add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web').using(settings.DATABASE_REPLICA), before, after, ebd, timezones)
add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web').using(
settings.DATABASE_REPLICA
).filter(
sales_channels__contains=self.request.sales_channel.identifier
), before, after, ebd, timezones)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
event__sales_channels__contains=self.request.sales_channel.identifier
).prefetch_related(
'event___settings_objects', 'event__organizer___settings_objects'
)), self.request).using(settings.DATABASE_REPLICA), before, after, ebd, timezones)
@@ -539,11 +545,16 @@ class WeekCalendarView(OrganizerViewMixin, EventListMixin, TemplateView):
def _events_by_day(self, before, after):
ebd = defaultdict(list)
timezones = set()
add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web').using(settings.DATABASE_REPLICA), before, after, ebd, timezones)
add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web').using(
settings.DATABASE_REPLICA
).filter(
sales_channels__contains=self.request.sales_channel.identifier
), before, after, ebd, timezones)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
event__sales_channels__contains=self.request.sales_channel.identifier
).prefetch_related(
'event___settings_objects', 'event__organizer___settings_objects'
)), self.request).using(settings.DATABASE_REPLICA), before, after, ebd, timezones)
@@ -556,7 +567,12 @@ class OrganizerIcalDownload(OrganizerViewMixin, View):
def get(self, request, *args, **kwargs):
events = list(
filter_qs_by_attr(
self.request.organizer.events.filter(is_public=True, live=True, has_subevents=False),
self.request.organizer.events.filter(
is_public=True,
live=True,
has_subevents=False,
sales_channels__contains=self.request.sales_channel.identifier
),
request
).order_by(
'date_from'
@@ -571,7 +587,8 @@ class OrganizerIcalDownload(OrganizerViewMixin, View):
event__is_public=True,
event__live=True,
is_public=True,
active=True
active=True,
event__sales_channels__contains=self.request.sales_channel.identifier
),
request
).prefetch_related(

View File

@@ -279,6 +279,11 @@ class WidgetAPIProductList(EventListMixin, View):
'error': gettext('This ticket shop is currently disabled.')
})
if request.sales_channel.identifier not in request.event.sales_channels:
return self.response({
'error': gettext('Tickets for this event cannot be purchased on this sales channel.')
})
self.subevent = None
if request.event.has_subevents:
if 'subevent' in kwargs:
@@ -417,7 +422,11 @@ class WidgetAPIProductList(EventListMixin, View):
if hasattr(self.request, 'event'):
add_subevents_for_days(
filter_qs_by_attr(self.request.event.subevents_annotated('web'), self.request),
filter_qs_by_attr(
self.request.event.subevents_annotated('web').filter(
event__sales_channels__contains=self.request.sales_channel.identifier
), self.request
),
before, after, ebd, set(), self.request.event,
kwargs.get('cart_namespace')
)
@@ -425,13 +434,18 @@ class WidgetAPIProductList(EventListMixin, View):
timezones = set()
add_events_for_days(
self.request,
filter_qs_by_attr(Event.annotated(self.request.organizer.events, 'web'), self.request),
filter_qs_by_attr(
Event.annotated(self.request.organizer.events, 'web').filter(
sales_channels__contains=self.request.sales_channel.identifier
), self.request
),
before, after, ebd, timezones
)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
event__sales_channels__contains=self.request.sales_channel.identifier
).prefetch_related(
'event___settings_objects', 'event__organizer___settings_objects'
)), self.request), before, after, ebd, timezones)

View File

@@ -85,7 +85,8 @@ TEST_EVENT_RES = {
],
'item_meta_properties': {
'day': 'Monday',
}
},
'sales_channels': ['web']
}

View File

@@ -34,7 +34,8 @@ class CartTestMixin:
organizer=self.orga, name='30C3', slug='30c3',
date_from=datetime.datetime(now().year + 1, 12, 26, tzinfo=datetime.timezone.utc),
live=True,
plugins="pretix.plugins.banktransfer"
plugins="pretix.plugins.banktransfer",
sales_channels=['web', 'bar']
)
self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'))
self.category = ItemCategory.objects.create(event=self.event, name="Everything", position=0)

View File

@@ -28,7 +28,7 @@ class EventTestMixin:
self.event = Event.objects.create(
organizer=self.orga, name='30C3', slug='30c3',
date_from=datetime.datetime(now().year + 1, 12, 26, tzinfo=datetime.timezone.utc),
live=True
live=True, sales_channels=['web', 'bar']
)
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
t = Team.objects.create(organizer=self.orga, can_change_event_settings=True)
@@ -1018,6 +1018,23 @@ class DeadlineTest(EventTestMixin, TestCase):
)
self.assertNotEqual(response.status_code, 403)
def test_saleschannel_disabled(self):
self.event.presale_start = None
self.event.presale_end = None
self.event.sales_channels = []
self.event.save()
response = self.client.get(
'/%s/%s/' % (self.orga.slug, self.event.slug)
)
self.assertEqual(response.status_code, 404)
response = self.client.post(
'/%s/%s/cart/add' % (self.orga.slug, self.event.slug),
{
'item_%d' % self.item.id: '1'
}
)
self.assertEqual(response.status_code, 404)
def test_in_time(self):
self.event.presale_start = now() - datetime.timedelta(days=1)
self.event.presale_end = now() + datetime.timedelta(days=1)

View File

@@ -121,6 +121,21 @@ class WidgetCartTest(CartTestMixin, TestCase):
self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[0].text)
self.assertIn('23', doc.select('.cart .cart-row')[0].select('.price')[1].text)
def test_saleschannel_disabled(self):
self.event.sales_channels = []
self.event.save()
response = self.client.get('/%s/%s/widget/product_list' % (self.orga.slug, self.event.slug))
data = json.loads(response.content.decode())
assert data == {
"error": "Tickets for this event cannot be purchased on this sales channel.",
'poweredby': '<a href="https://pretix.eu" target="_blank" rel="noopener">event ticketing powered by pretix</a>',
}
self.assertEqual(response.status_code, 200)
response = self.client.post('/%s/%s/w/aaaaaaaaaaaaaaab/cart/add' % (self.orga.slug, self.event.slug), {
'item_%d' % self.ticket.id: '1'
}, follow=True)
self.assertEqual(response.status_code, 404)
def test_product_list_view(self):
response = self.client.get('/%s/%s/widget/product_list' % (self.orga.slug, self.event.slug))
assert response['Access-Control-Allow-Origin'] == '*'