Added an organizer overview page

This commit is contained in:
Raphael Michel
2015-10-22 17:45:19 +02:00
parent 0b4cae07c4
commit 232c42b265
23 changed files with 400 additions and 92 deletions

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0002_auto_20151021_1412'),
]
operations = [
migrations.AddField(
model_name='event',
name='is_public',
field=models.BooleanField(help_text="If selected, this event may show up on the ticket system's start page or an organization profile.", default=False, verbose_name='Visible in public lists'),
),
]

View File

@@ -73,6 +73,10 @@ class Event(Versionable):
date_from = models.DateTimeField(verbose_name=_("Event start time"))
date_to = models.DateTimeField(null=True, blank=True,
verbose_name=_("Event end time"))
is_public = models.BooleanField(default=False,
verbose_name=_("Visible in public lists"),
help_text=_("If selected, this event may show up on the ticket system's start page "
"or an organization profile."))
presale_end = models.DateTimeField(
null=True, blank=True,
verbose_name=_("End of presale"),

View File

View File

@@ -0,0 +1,10 @@
from django import template
register = template.Library()
@register.simple_tag
def url_replace(request, field, value):
dict_ = request.GET.copy()
dict_[field] = value
return dict_.urlencode()

View File

@@ -56,6 +56,7 @@ class EventUpdateForm(VersionedModelForm):
'currency',
'date_from',
'date_to',
'is_public',
'presale_start',
'presale_end',
]

View File

@@ -12,6 +12,7 @@
{% bootstrap_field form.date_from layout="horizontal" %}
{% bootstrap_field form.date_to layout="horizontal" %}
{% bootstrap_field form.currency layout="horizontal" %}
{% bootstrap_field form.is_public layout="horizontal" %}
</fieldset>
<fieldset>
<legend>{% trans "Display settings" %}</legend>

View File

@@ -1,10 +1,11 @@
{% load i18n %}
{% load urlreplace %}
{% if is_paginated %}
<nav class="text-center">
<ul class="pagination">
{% if page_obj.has_previous %}
<li>
<a href="?page={{ page_obj.previous_page_number }}">
<a href="?{% url_replace request 'page' page_obj.previous_page_number %}">
<span>&laquo;</span>
</a>
</li>
@@ -16,7 +17,7 @@
</a></li>
{% if page_obj.has_next %}
<li>
<a href="?page={{ page_obj.next_page_number }}">
<a href="?{% url_replace request 'page' page_obj.next_page_number %}">
<span>&raquo;</span>
</a>
</li>

View File

@@ -1,11 +1,14 @@
from django.conf.urls import include, url
from pretix.presale.urls import event_patterns, locale_patterns
from pretix.presale.urls import (
event_patterns, locale_patterns, organizer_patterns,
)
from pretix.urls import common_patterns
presale_patterns_main = [
url(r'', include(locale_patterns + [
url(r'^(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include(event_patterns))
url(r'^(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include(event_patterns)),
url(r'^(?P<organizer>[^/]+)/', include(organizer_patterns))
], namespace='presale'))
]

View File

@@ -1,12 +1,14 @@
from django.conf.urls import include, url
from pretix.presale.urls import event_patterns, locale_patterns
from pretix.presale.urls import (
event_patterns, locale_patterns, organizer_patterns,
)
from pretix.urls import common_patterns
print(event_patterns)
presale_patterns = [
url(r'', include(locale_patterns + [
url(r'^(?P<event>[^/]+)/', include(event_patterns))
url(r'^(?P<event>[^/]+)/', include(event_patterns)),
url(r'', include(organizer_patterns))
], namespace='presale'))
]

View File

@@ -43,7 +43,7 @@ def eventurl(parser, token):
"""
Similar to {% url %} in the same way that eventreverse() is similar to reverse().
Takes an event object, an url name and optional keyword arguments
Takes an event or organizer object, an url name and optional keyword arguments
"""
bits = token.split_contents()
if len(bits) < 3:

View File

@@ -3,12 +3,14 @@ from urllib.parse import urljoin, urlsplit
from django.conf import settings
from django.core.urlresolvers import reverse
from pretix.base.models import Event, Organizer
def get_domain(event):
c = event.organizer.get_cache()
def get_domain(organizer):
c = organizer.get_cache()
domain = c.get('domain')
if domain is None:
domains = event.organizer.domains.all()
domains = organizer.domains.all()
domain = domains[0].domainname if domains else None
c.set('domain', domain or 'none')
elif domain == 'none':
@@ -16,16 +18,24 @@ def get_domain(event):
return domain
def eventreverse(event, name, kwargs=None):
def eventreverse(obj, name, kwargs=None):
"""
Works similar to django.core.urlresolvers.reverse but takes into account that some
organizers might have their own (sub)domain instead of a subpath.
:param obj: An event or organizer
"""
from pretix.multidomain import subdomain_urlconf, maindomain_urlconf
kwargs = kwargs or {}
kwargs['event'] = event.slug
domain = get_domain(event)
if isinstance(obj, Event):
kwargs['event'] = obj.slug
organizer = obj.organizer
elif isinstance(obj, Organizer):
organizer = obj
else:
raise TypeError('obj should be Event or Organizer')
domain = get_domain(organizer)
if domain:
if 'organizer' in kwargs:
del kwargs['organizer']
@@ -36,12 +46,12 @@ def eventreverse(event, name, kwargs=None):
domain = '%s:%d' % (domain, siteurlsplit.port)
return urljoin('%s://%s' % (siteurlsplit.scheme, domain), path)
kwargs['organizer'] = event.organizer.slug
kwargs['organizer'] = organizer.slug
return reverse(name, kwargs=kwargs, urlconf=maindomain_urlconf)
def build_absolute_uri(event, urlname, kwargs=None):
reversedurl = eventreverse(event, urlname, kwargs)
def build_absolute_uri(obj, urlname, kwargs=None):
reversedurl = eventreverse(obj, urlname, kwargs)
if '://' in reversedurl:
return reversedurl
return urljoin(settings.SITE_URL, reversedurl)

View File

@@ -5,7 +5,7 @@ from django.http import Http404
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event
from pretix.base.models import Event, Organizer
from pretix.multidomain.urlreverse import get_domain
@@ -13,11 +13,10 @@ class EventMiddleware:
def process_request(self, request):
url = resolve(request.path_info)
url_namespace = url.namespace
url_name = url.url_name
if url_namespace != 'presale':
return
if 'event.' in url_name and 'event' in url.kwargs:
if 'organizer' in url.kwargs or 'event' in url.kwargs:
try:
if hasattr(request, 'organizer'):
# We are on an organizer's custom domain
@@ -31,18 +30,24 @@ class EventMiddleware:
slug=url.kwargs['event'],
organizer=request.organizer,
).select_related('organizer')[0]
request.organizer = request.event.organizer
else:
# We are on our main domain
if 'organizer' not in url.kwargs:
raise Http404(_('The selected event was not found.'))
request.event = Event.objects.current.filter(
slug=url.kwargs['event'],
organizer__slug=url.kwargs['organizer']
).select_related('organizer')[0]
if 'event' in url.kwargs and 'organizer' in url.kwargs:
request.event = Event.objects.current.filter(
slug=url.kwargs['event'],
organizer__slug=url.kwargs['organizer']
).select_related('organizer')[0]
request.organizer = request.event.organizer
elif 'organizer' in url.kwargs:
request.organizer = Organizer.objects.current.filter(
slug=url.kwargs['organizer']
)[0]
else:
raise Http404()
# If this organizer has a custom domain, send the user there
domain = get_domain(request.event)
domain = get_domain(request.organizer)
if domain:
if request.port and request.port not in (80, 443):
domain = '%s:%d' % (domain, request.port)
@@ -50,7 +55,7 @@ class EventMiddleware:
return redirect(urljoin('%s://%s' % (request.scheme, domain), path))
except IndexError:
raise Http404(_('The selected event was not found.'))
raise Http404(_('The selected event or organizer was not found.'))
if '_' not in request.session:
# We need to create session even if we do not yet store something there, because we need the session

View File

@@ -0,0 +1,49 @@
{% load compress %}
{% load staticfiles %}
{% load i18n %}
<!DOCTYPE html>
<html>
<head>
<title>{% block thetitle %}{% endblock %}</title>
{% compress css %}
<link rel="stylesheet" type="text/less" href="{% static "pretixpresale/less/main.less" %}" />
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script>
<script type="text/javascript" src="{% static "bootstrap/dist/js/bootstrap.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/asynctask.js" %}"></script>
{% endcompress %}
{{ html_head|safe }}
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="container">
{% block page %}
{% endblock %}
</div>
<footer>
{% block footer %}
{% endblock %}
{% with "href='http://pretix.eu'" as a_attr %}
{% blocktrans trimmed %}
powered by <a {{ a_attr }}>pretix</a>
{% endblocktrans %}
{% endwith %}
</footer>
<script type="text/javascript">
var default_loading_message = '{% trans "We are processing your request…" %}';
</script>
<div id="loadingmodal">
<i class="fa fa-cog big-rotating-icon"></i>
<h1>{% trans "We are processing your request…" %}</h1>
<p>
{% trans "If this takes longer than a few minutes, please contact us." %}
</p>
</div>
{% compress js %}
<script type="text/javascript" src="{% static "lightbox/js/lightbox.min.js" %}"></script>
{% endcompress %}
</body>
</html>

View File

@@ -1,26 +1,11 @@
{% load compress %}
{% load staticfiles %}
{% extends "pretixpresale/base.html" %}
{% load i18n %}
{% load static %}
{% load eventurl %}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}{% if url_name != "event.index" %} :: {% endif %}{{ event.name }}</title>
{% compress css %}
<link rel="stylesheet" type="text/less" href="{% static "pretixpresale/less/main.less" %}" />
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "js/jquery.formset.js" %}"></script>
<script type="text/javascript" src="{% static "bootstrap/dist/js/bootstrap.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/main.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/asynctask.js" %}"></script>
{% endcompress %}
{{ html_head|safe }}
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="container event">
{% block thetitle %}
{% block title %}{% endblock %}{% if url_name != "event.index" %} :: {% endif %}{{ event.name }}
{% endblock %}
{% block page %}
<div class="page-header">
<h1 class="pull-left">
<a href="{% eventurl event "presale:event.index" %}">{{ event.name }}</a>
@@ -50,8 +35,8 @@
{% endif %}
{% block content %}
{% endblock %}
</div>
<footer>
{% endblock %}
{% block footer %}
{% if request.event.settings.contact_mail %}
<a href="mailto:{{ request.event.settings.contact_mail }}">{% trans "Contact event organizer" %}</a> &middot;
{% endif %}
@@ -59,24 +44,4 @@
<a href="{{ request.event.settings.imprint_url }}" target="_blank">{% trans "Imprint" %}</a>
&middot;
{% endif %}
{% with "href='http://pretix.eu'" as a_attr %}
{% blocktrans trimmed %}
powered by <a {{ a_attr }}>pretix</a>
{% endblocktrans %}
{% endwith %}
</footer>
<script type="text/javascript">
var default_loading_message = '{% trans "We are processing your request…" %}';
</script>
<div id="loadingmodal">
<i class="fa fa-cog big-rotating-icon"></i>
<h1>{% trans "We are processing your request…" %}</h1>
<p>
{% trans "If this takes longer than a few minutes, please contact us." %}
</p>
</div>
{% compress js %}
<script type="text/javascript" src="{% static "lightbox/js/lightbox.min.js" %}"></script>
{% endcompress %}
</body>
</html>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends "pretixpresale/base.html" %}
{% load i18n %}
{% load static %}
{% load eventurl %}
{% block thetitle %}
{% block title %}{% endblock %}{% if url_name != "organizer.index" %} :: {% endif %}{{ organizer.name }}
{% endblock %}
{% block page %}
<div class="page-header">
<h1 class="pull-left">
<a href="{% eventurl organizer "presale:organizer.index" %}">{{ organizer.name }}</a>
</h1>
<div class="clearfix"></div>
</div>
{% if messages %}
{% for message in messages %}
<div class="alert {{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% block content %}
{% endblock %}
{% endblock %}
{% block footer %}
{% endblock %}

View File

@@ -0,0 +1,61 @@
{% extends "pretixpresale/organizers/base.html" %}
{% load i18n %}
{% load eventurl %}
{% block title %}Event list{% endblock %}
{% block content %}
{% if "old" in request.GET %}
<h3>Past events</h3>
<p>
<small><a href="?">{% trans "Show upcoming" %}</a></small>
</p>
{% else %}
<h3>Upcoming events </h3>
<p>
<small><a href="?old=1">{% trans "Show past events" %}</a></small>
</p>
{% endif %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Date" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for e in events %}{% eventurl e "presale:event.index" as url %}
<tr>
<td><a href="{{ url }}">{{ e.name }}</a></td>
<td>
{{ e.date_from|date:"DATE_FORMAT" }}
{% if e.settings.show_date_to %} {{ e.date_to|date:"DATE_FORMAT" }}{% endif %}
</td>
<td class="text-right">
<a class="btn btn-primary" href="{{ url }}">
{% if e.presale_is_running %}{% trans "Buy tickets" %}
{% else %}{% trans "More info" %}
{% endif %}
</a>
</td>
</tr>
{% empty %}
{% if "old" in request.GET %}
<tr>
<td colspan="3">
<em>{% trans "No archived events found." %}</em>
</td>
</tr>
{% else %}
<tr>
<td colspan="3">
<em>{% trans "No public upcoming events found." %}</em>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% include "pretixpresale/pagination.html" %}
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% load i18n %}
{% load urlreplace %}
{% if is_paginated %}
<nav class="text-center">
<ul class="pagination">
{% if page_obj.has_previous %}
<li>
<a href="?{% url_replace request 'page' page_obj.previous_page_number %}">
<span>&laquo;</span>
</a>
</li>
{% endif %}
<li class="page-current"><a>
{% blocktrans trimmed with page=page_obj.number of=page_obj.paginator.num_pages %}
Page {{ page }} of {{ of }}
{% endblocktrans %}
</a></li>
{% if page_obj.has_next %}
<li>
<a href="?{% url_replace request 'page' page_obj.next_page_number %}">
<span>&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}

View File

@@ -5,6 +5,7 @@ import pretix.presale.views.checkout
import pretix.presale.views.event
import pretix.presale.views.locale
import pretix.presale.views.order
import pretix.presale.views.organizer
# This is not a valid Django URL configuration, as the final
# configuration is done by the pretix.multidomain package.
@@ -37,6 +38,10 @@ event_patterns = [
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
]
organizer_patterns = [
url(r'^$', pretix.presale.views.organizer.OrganizerIndex.as_view(), name='organizer.index'),
]
locale_patterns = [
url(r'^locale/set$', pretix.presale.views.locale.LocaleSet.as_view(), name='locale.set'),
]

View File

@@ -1,30 +1,11 @@
from datetime import timedelta
from itertools import groupby
from django.contrib.auth.views import redirect_to_login
from django.utils.functional import cached_property
from django.utils.timezone import now
from pretix.base.models import CartPosition
from pretix.base.signals import register_payment_providers
from pretix.multidomain.urlreverse import eventreverse
def login_required(view_func):
def _wrapped_view(request, *args, **kwargs):
if request.user.is_authenticated():
return view_func(request, *args, **kwargs)
path = request.path
return redirect_to_login(path, eventreverse(request.event, 'presale:event.checkout.login'), 'next')
return _wrapped_view
class LoginRequiredMixin:
@classmethod
def as_view(cls, **initkwargs):
view = super().as_view(**initkwargs)
return login_required(view)
class CartMixin:
@@ -116,3 +97,10 @@ class EventViewMixin:
context = super().get_context_data(**kwargs)
context['event'] = self.request.event
return context
class OrganizerViewMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['organizer'] = self.request.organizer
return context

View File

@@ -0,0 +1,25 @@
from django.db.models import Q
from django.utils.timezone import now
from django.views.generic import ListView
from pretix.base.models import Event
from pretix.presale.views import OrganizerViewMixin
class OrganizerIndex(OrganizerViewMixin, ListView):
model = Event
context_object_name = 'events'
template_name = 'pretixpresale/organizers/index.html'
paginate_by = 1
def get_queryset(self):
query = Q(is_public=True)
if "old" in self.request.GET:
query &= Q(Q(date_from__lte=now()) & Q(date_to__lte=now()))
order = '-date_from'
else:
query &= Q(Q(date_from__gte=now()) | Q(date_to__gte=now()))
order = 'date_from'
return Event.objects.current.filter(
Q(organizer=self.request.organizer) & query
).order_by(order)

View File

@@ -47,6 +47,14 @@ def test_event_with_custom_domain_on_main_domain(env, client):
assert r['Location'] == 'http://foobar/2015/'
@pytest.mark.django_db
def test_organizer_with_custom_domain_on_main_domain(env, client):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
r = client.get('/mrmcd/', HTTP_HOST='example.com')
assert r.status_code == 302
assert r['Location'] == 'http://foobar'
@pytest.mark.django_db
def test_event_on_custom_domain_only_with_wrong_organizer(env, client):
organizer2 = Organizer.objects.create(name='Dummy', slug='dummy')

View File

@@ -22,6 +22,7 @@ def env():
@pytest.mark.django_db
def test_event_main_domain_front_page(env):
assert eventreverse(env[1], 'presale:event.index') == '/mrmcd/2015/'
assert eventreverse(env[0], 'presale:organizer.index') == '/mrmcd/'
@pytest.mark.django_db
@@ -39,6 +40,7 @@ def test_event_main_domain_kwargs(env):
def test_event_custom_domain_front_page(env):
KnownDomain.objects.create(domainname='foobar', organizer=env[0])
assert eventreverse(env[1], 'presale:event.index') == 'http://foobar/2015/'
assert eventreverse(env[0], 'presale:organizer.index') == 'http://foobar/'
@pytest.mark.django_db

View File

@@ -0,0 +1,96 @@
from datetime import timedelta
import pytest
from django.utils.timezone import now
from pretix.base.models import Event, Organizer
@pytest.fixture
def env():
o = Organizer.objects.create(name='MRMCD e.V.', slug='mrmcd')
event = Event.objects.create(
organizer=o, name='MRMCD2015', slug='2015',
date_from=now() + timedelta(days=10)
)
return o, event
@pytest.mark.django_db
def test_organizer_page_shown(env, client):
r = client.get('/mrmcd/')
assert r.status_code == 200
assert 'MRMCD e.V.' in r.rendered_content
@pytest.mark.django_db
def test_public_event_on_page(env, client):
env[1].is_public = True
env[1].save()
r = client.get('/mrmcd/')
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
env[1].save()
r = client.get('/mrmcd/')
assert 'MRMCD2015' not in r.rendered_content
@pytest.mark.django_db
def test_running_event_on_current_page(env, client):
env[1].date_from = now() - timedelta(days=2)
env[1].date_to = now() + timedelta(days=2)
env[1].is_public = True
env[1].save()
r = client.get('/mrmcd/')
assert 'MRMCD2015' in r.rendered_content
@pytest.mark.django_db
def test_past_event_shown_on_archive_page(env, client):
env[1].date_from = now() - timedelta(days=2)
env[1].date_to = now() - timedelta(days=2)
env[1].is_public = True
env[1].save()
r = client.get('/mrmcd/?old=1')
assert 'MRMCD2015' in r.rendered_content
@pytest.mark.django_db
def test_event_not_shown_on_archive_page(env, client):
env[1].is_public = True
env[1].save()
r = client.get('/mrmcd/?old=1')
assert 'MRMCD2015' not in r.rendered_content
@pytest.mark.django_db
def test_past_event_not_shown(env, client):
env[1].date_from = now() - timedelta(days=2)
env[1].date_to = now() - timedelta(days=2)
env[1].is_public = True
env[1].save()
r = client.get('/mrmcd/')
assert 'MRMCD2015' not in r.rendered_content
@pytest.mark.django_db
def test_empty_message(env, client):
env[1].is_public = False
env[1].save()
r = client.get('/mrmcd/')
assert 'No public upcoming events found' in r.rendered_content
@pytest.mark.django_db
def test_different_organizer_not_shown(env, client):
o = Organizer.objects.create(name='CCC e.V.', slug='ccc')
Event.objects.create(
organizer=o, name='32C3', slug='32c3',
date_from=now() + timedelta(days=10), is_public=True
)
r = client.get('/mrmcd/')
assert '32C3' not in r.rendered_content