diff --git a/src/pretix/presale/templates/pretixpresale/organizers/index.html b/src/pretix/presale/templates/pretixpresale/organizers/index.html
index b1be1b095a..14fce5205d 100644
--- a/src/pretix/presale/templates/pretixpresale/organizers/index.html
+++ b/src/pretix/presale/templates/pretixpresale/organizers/index.html
@@ -2,6 +2,7 @@
{% load i18n %}
{% load rich_text %}
{% load eventurl %}
+{% load urlreplace %}
{% block title %}{% trans "Event list" %}{% endblock %}
{% block content %}
@@ -16,26 +17,26 @@
{% endif %}
diff --git a/src/pretix/presale/views/organizer.py b/src/pretix/presale/views/organizer.py
index a19a2d385c..e3ccbeaf22 100644
--- a/src/pretix/presale/views/organizer.py
+++ b/src/pretix/presale/views/organizer.py
@@ -4,7 +4,7 @@ from datetime import date, datetime, timedelta
import pytz
from django.conf import settings
-from django.db.models import Q
+from django.db.models import Exists, OuterRef, Q
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.utils.timezone import now
@@ -14,12 +14,73 @@ from django.views.generic import ListView, TemplateView
from pytz import UTC
from pretix.base.i18n import language
-from pretix.base.models import Event, SubEvent
+from pretix.base.models import (
+ Event, EventMetaValue, SubEvent, SubEventMetaValue,
+)
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.ical import get_ical
from pretix.presale.views import OrganizerViewMixin
+def filter_qs_by_attr(qs, request):
+ """
+ We'll allow to filter the event list using attributes defined in the event meta data
+ models in the format ?attr[meta_name]=meta_value
+ """
+ attrs = {}
+ for i, item in enumerate(request.GET.items()):
+ k, v = item
+ if k.startswith("attr[") and k.endswith("]"):
+ attrs[k[5:-1]] = v
+
+ props = {
+ p.name: p for p in request.organizer.meta_properties.filter(
+ name__in=attrs.keys()
+ )
+ }
+
+ for i, item in enumerate(attrs.items()):
+ attr, v = item
+ emv_with_value = EventMetaValue.objects.filter(
+ event=OuterRef('event' if qs.model == SubEvent else 'pk'),
+ property__name=attr,
+ value=v
+ )
+ emv_with_any_value = EventMetaValue.objects.filter(
+ event=OuterRef('event' if qs.model == SubEvent else 'pk'),
+ property__name=attr,
+ )
+ if qs.model == SubEvent:
+ semv_with_value = SubEventMetaValue.objects.filter(
+ subevent=OuterRef('pk'),
+ property__name=attr,
+ value=v
+ )
+ semv_with_any_value = SubEventMetaValue.objects.filter(
+ subevent=OuterRef('pk'),
+ property__name=attr,
+ )
+
+ prop = props[attr]
+ annotations = {'attr_{}'.format(i): Exists(emv_with_value)}
+ if qs.model == SubEvent:
+ annotations['attr_{}_sub'.format(i)] = Exists(semv_with_value)
+ annotations['attr_{}_sub_any'.format(i)] = Exists(semv_with_any_value)
+ filters = Q(**{'attr_{}_sub'.format(i): True})
+ filters |= Q(Q(**{'attr_{}_sub_any'.format(i): False}) & Q(**{'attr_{}'.format(i): True}))
+ if prop.default == v:
+ annotations['attr_{}_any'.format(i)] = Exists(emv_with_any_value)
+ filters |= Q(Q(**{'attr_{}_sub_any'.format(i): False}) & Q(**{'attr_{}_any'.format(i): False}))
+ else:
+ filters = Q(**{'attr_{}'.format(i): True})
+ if prop.default == v:
+ annotations['attr_{}_any'.format(i)] = Exists(emv_with_any_value)
+ filters |= Q(**{'attr_{}_any'.format(i): False})
+
+ qs = qs.annotate(**annotations).filter(filters)
+ return qs
+
+
class OrganizerIndex(OrganizerViewMixin, ListView):
model = Event
context_object_name = 'events'
@@ -43,12 +104,14 @@ class OrganizerIndex(OrganizerViewMixin, ListView):
else:
query &= Q(Q(date_from__gte=now()) | Q(date_to__gte=now()))
order = 'date_from'
- return Event.objects.filter(
+ qs = Event.objects.filter(
Q(organizer=self.request.organizer) & query
- ).order_by(order)
+ )
+ qs = filter_qs_by_attr(qs, self.request)
+ return qs.order_by(order)
-def add_events_for_days(organizer, before, after, ebd, timezones):
+def add_events_for_days(request, organizer, before, after, ebd, timezones):
qs = organizer.events.filter(is_public=True, live=True, has_subevents=False).filter(
Q(Q(date_to__gte=before) & Q(date_from__lte=after)) |
Q(Q(date_from__lte=after) & Q(date_to__gte=before)) |
@@ -58,6 +121,7 @@ def add_events_for_days(organizer, before, after, ebd, timezones):
).prefetch_related(
'_settings_objects', 'organizer___settings_objects'
)
+ qs = filter_qs_by_attr(qs, request)
for event in qs:
timezones.add(event.settings.timezones)
tz = pytz.timezone(event.settings.timezone)
@@ -161,19 +225,19 @@ class CalendarView(OrganizerViewMixin, TemplateView):
self.year = now().year
self.month = now().month
else:
- next_ev = Event.objects.filter(
+ next_ev = filter_qs_by_attr(Event.objects.filter(
live=True,
is_public=True,
date_from__gte=now(),
has_subevents=False
- ).order_by('date_from').first()
- next_sev = SubEvent.objects.filter(
+ ), self.request).order_by('date_from').first()
+ next_sev = filter_qs_by_attr(SubEvent.objects.filter(
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
active=True,
date_from__gte=now()
- ).select_related('event').order_by('date_from').first()
+ ), self.request).select_related('event').order_by('date_from').first()
datetime_from = None
if (next_ev and next_sev and next_sev.date_from < next_ev.date_from) or (next_sev and not next_ev):
@@ -213,14 +277,14 @@ class CalendarView(OrganizerViewMixin, TemplateView):
def _events_by_day(self, before, after):
ebd = defaultdict(list)
timezones = set()
- add_events_for_days(self.request.organizer, before, after, ebd, timezones)
- add_subevents_for_days(SubEvent.objects.filter(
+ add_events_for_days(self.request, self.request.organizer, before, after, ebd, timezones)
+ add_subevents_for_days(filter_qs_by_attr(SubEvent.objects.filter(
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
'event___settings_objects', 'event__organizer___settings_objects'
- ), before, after, ebd, timezones)
+ ), self.request), before, after, ebd, timezones)
self._multiple_timezones = len(timezones) > 1
return ebd
@@ -229,18 +293,24 @@ class CalendarView(OrganizerViewMixin, TemplateView):
class OrganizerIcalDownload(OrganizerViewMixin, View):
def get(self, request, *args, **kwargs):
events = list(
- self.request.organizer.events.filter(is_public=True, live=True, has_subevents=False).order_by(
+ filter_qs_by_attr(
+ self.request.organizer.events.filter(is_public=True, live=True, has_subevents=False),
+ request
+ ).order_by(
'date_from'
).prefetch_related(
'_settings_objects', 'organizer___settings_objects'
)
)
events += list(
- SubEvent.objects.filter(
- event__organizer=self.request.organizer,
- event__is_public=True,
- event__live=True,
- active=True
+ filter_qs_by_attr(
+ SubEvent.objects.filter(
+ event__organizer=self.request.organizer,
+ event__is_public=True,
+ event__live=True,
+ active=True
+ ),
+ request
).prefetch_related(
'event___settings_objects', 'event__organizer___settings_objects'
).order_by(
diff --git a/src/tests/presale/test_organizer_page.py b/src/tests/presale/test_organizer_page.py
index edbf3eb5a0..e085c5eaad 100644
--- a/src/tests/presale/test_organizer_page.py
+++ b/src/tests/presale/test_organizer_page.py
@@ -33,6 +33,25 @@ def test_public_event_on_page(env, client):
assert 'MRMCD2015' in r.rendered_content
+@pytest.mark.django_db
+def test_attributes_on_page(env, client):
+ env[1].is_public = True
+ env[1].save()
+
+ prop = env[0].meta_properties.create(name='loc', default='HH')
+ propval = env[1].meta_values.create(value='HD', property=prop)
+
+ r = client.get('/mrmcd/?attr[loc]=HD')
+ assert 'MRMCD2015' in r.rendered_content
+ r = client.get('/mrmcd/?attr[loc]=MA')
+ assert 'MRMCD2015' not in r.rendered_content
+ r = client.get('/mrmcd/?attr[loc]=HH')
+ assert 'MRMCD2015' not in r.rendered_content
+ propval.delete()
+ r = client.get('/mrmcd/?attr[loc]=HH')
+ assert 'MRMCD2015' in r.rendered_content
+
+
@pytest.mark.django_db
def test_non_public_event_not_on_page(env, client):
env[1].is_public = False
@@ -118,6 +137,24 @@ def test_calendar(env, client):
assert 'October 2017' in r.rendered_content
+@pytest.mark.django_db
+def test_attributes_in_calendar(env, client):
+ env[0].settings.event_list_type = 'calendar'
+ e = Event.objects.create(
+ organizer=env[0], name='MRMCD2017', slug='2017',
+ date_from=datetime(now().year + 1, 9, 1, tzinfo=UTC),
+ live=True, is_public=True
+ )
+ prop = env[0].meta_properties.create(name='loc')
+ e.meta_values.create(value='HD', property=prop)
+
+ r = client.get('/mrmcd/?attr[loc]=HD&style=calendar')
+ print(r.rendered_content)
+ assert 'MRMCD2017' in r.rendered_content
+ r = client.get('/mrmcd/?attr[loc]=MA&style=calendar')
+ assert 'MRMCD2017' not in r.rendered_content
+
+
@pytest.mark.django_db
def test_ics(env, client):
e = Event.objects.create(
@@ -144,3 +181,39 @@ def test_ics_subevents(env, client):
r = client.get('/mrmcd/events/ical/')
assert b'MRMCD2017' not in r.content
assert b'SE1' in r.content
+
+
+@pytest.mark.django_db
+def test_ics_subevents_attributes(env, client):
+ e0 = Event.objects.create(
+ organizer=env[0], name='DS2017', slug='DS2017',
+ date_from=datetime(now().year + 1, 9, 1, tzinfo=UTC),
+ live=True, is_public=True
+ )
+ e = Event.objects.create(
+ organizer=env[0], name='MRMCD2017', slug='2017',
+ date_from=datetime(now().year + 1, 9, 1, tzinfo=UTC),
+ live=True, is_public=True, has_subevents=True
+ )
+ se1 = e.subevents.create(date_from=now(), name='SE1', active=True)
+
+ prop = env[0].meta_properties.create(name='loc', default='HH')
+ e0.meta_values.create(value='MA', property=prop)
+ propval = se1.meta_values.create(value='HD', property=prop)
+ r = client.get('/mrmcd/events/ical/?attr[loc]=HD')
+ assert b'SE1' in r.content
+ assert b'DS2017' not in r.content
+ r = client.get('/mrmcd/events/ical/?attr[loc]=MA')
+ assert b'SE1' not in r.content
+ assert b'DS2017' in r.content
+
+ r = client.get('/mrmcd/events/ical/?attr[loc]=HH')
+ assert b'SE1' not in r.content
+ propval.delete()
+ r = client.get('/mrmcd/events/ical/?attr[loc]=HH')
+ assert b'SE1' in r.content
+ e.meta_values.create(value='B', property=prop)
+ r = client.get('/mrmcd/events/ical/?attr[loc]=HH')
+ assert b'SE1' not in r.content
+ r = client.get('/mrmcd/events/ical/?attr[loc]=B')
+ assert b'SE1' in r.content