Add event meta filter to organizer page

This commit is contained in:
Raphael Michel
2017-10-28 21:54:00 +02:00
parent 9767243a6d
commit 2bcb0b0ac1
6 changed files with 193 additions and 35 deletions

View File

@@ -667,7 +667,7 @@ class EventMetaProperty(LoggedModel):
name = models.CharField(
max_length=50, db_index=True,
help_text=_(
"Can not contain spaces or special characters execpt underscores"
"Can not contain spaces or special characters except underscores"
),
validators=[
RegexValidator(

View File

@@ -4,7 +4,17 @@ register = template.Library()
@register.simple_tag
def url_replace(request, field, value):
def url_replace(request, *pairs):
dict_ = request.GET.copy()
dict_[field] = value
return dict_.urlencode()
key = None
for p in pairs:
if key is None:
key = p
else:
if p == "":
if key in dict_:
del dict_[key]
else:
dict_[key] = p
key = None
return dict_.urlencode(safe='[]')

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load rich_text %}
{% load eventurl %}
{% load urlreplace %}
{% block title %}{% trans "Event overview" %}{% endblock %}
{% block content %}
{% if organizer_homepage_text %}
@@ -11,21 +12,26 @@
{% endif %}
<h3>{{ date|date:"F Y" }}</h3>
<form class="form-inline" method="get" id="monthselform" action="{% eventurl request.organizer "presale:organizer.index" %}">
<input type="hidden" name="style" value="calendar">
{% for f, v in request.GET.items %}
{% if f != "month" and f != "year" %}
<input type="hidden" name="{{ f }}" value="{{ v }}">
{% endif %}
{% endfor %}
<div class="row">
<div class="col-sm-4 col-xs-12">
<div class="btn-group" role="group">
<a href="{% eventurl request.organizer "presale:organizer.index" %}?style=list" type="button" class="btn btn-default">
<a href="?{% url_replace request "style" "list" %}" type="button" class="btn btn-default">
<span class="fa fa-list"></span>
{% trans "List" %}
</a>
<a href="{% eventurl request.organizer "presale:organizer.index" %}?style=calendar" type="button"
<a href="?{% url_replace request "style" "calendar" %}"
type="button"
class="btn btn-default active">
<span class="fa fa-calendar"></span>
{% trans "Calendar" %}
</a>
</div>
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?locale={{ request.LANGUAGE_CODE }}"
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?{% url_replace request "locale" request.LANGUAGE_CODE "style" "" "month" "" "year" "" %}"
class="btn btn-default">
<span class="fa fa-calendar-plus-o"></span>
{% trans "iCal" %}
@@ -47,13 +53,11 @@
</button>
</div>
<div class="col-sm-4 hidden-xs text-right">
<a href="{% eventurl request.organizer "presale:organizer.index" %}?style=calendar&year={{ before.year }}&month={{ before.month }}"
class="btn btn-default">
<a href="?{% url_replace request "year" before.year "month" before.month %}" class="btn btn-default">
<span class="fa fa-arrow-left"></span>
{{ before|date:"F Y" }}
</a>
<a href="{% eventurl request.organizer "presale:organizer.index" %}?style=calendar&year={{ after.year }}&month={{ after.month }}"
class="btn btn-default">
<a href="?{% url_replace request "year" after.year "month" after.month %}" class="btn btn-default">
<span class="fa fa-arrow-right"></span>
{{ after|date:"F Y" }}
</a>

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load rich_text %}
{% load eventurl %}
{% load urlreplace %}
{% block title %}{% trans "Event list" %}{% endblock %}
{% block content %}
<div>
@@ -16,26 +17,26 @@
{% endif %}
<div>
<div class="btn-group" role="group">
<a href="{% eventurl request.organizer "presale:organizer.index" %}?style=list" type="button"
<a href="?{% url_replace request "style" "list" %}" type="button"
class="btn btn-default active">
<span class="fa fa-list"></span>
{% trans "List" %}
</a>
<a href="{% eventurl request.organizer "presale:organizer.index" %}?style=calendar" type="button"
<a href="?{% url_replace request "style" "calendar" %}" type="button"
class="btn btn-default">
<span class="fa fa-calendar"></span>
{% trans "Calendar" %}
</a>
</div>
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?locale={{ request.LANGUAGE_CODE }}"
<a href="{% eventurl request.organizer "presale:organizer.ical" %}?{% eventurl request.organizer "presale:organizer.ical" %}?{% url_replace request "locale" request.LANGUAGE_CODE "style" "" "month" "" "year" "" %}"
class="btn btn-default">
<span class="fa fa-calendar-plus-o"></span>
{% trans "iCal" %}
</a>
{% if "old" in request.GET %}
<a href="?style=list" class="btn btn-link">{% trans "Show upcoming" %}</a>
<a href="?{% url_replace request "old" "" %}" class="btn btn-link">{% trans "Show upcoming" %}</a>
{% else %}
<a href="?style=list&old=1" class="btn btn-link">{% trans "Show past events" %}</a>
<a href="?{% url_replace request "old" "1" %}" class="btn btn-link">{% trans "Show past events" %}</a>
{% endif %}
</div>
<div class="table-responsive">

View File

@@ -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(

View File

@@ -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