Add sub-events and relative date settings (#503)

* Data model

* little crud

* SubEventItemForm etc

* Drop SubEventItem.active, quota editor

* Fix failing tests

* First frontend stuff

* Addons form stuff

* Quota calculation

* net price display on EventIndex

* Add tests, solve some bugs

* Correct quota selection in more places, consolidate pricing logic

* Fix failing quota tests

* Fix TypeError

* Add tests for checkout

* Fixed a bug in QuotaForm

* Prevent immutable cart if a quota was removed from an item

* Add tests for pricing

* Handle waiting list

* Filter in check-in list

* Fixed import lost in rebase

* Fix waiting list widget

* Voucher management

* Voucher redemption

* Fix broken tests

* Add subevents to OrderChangeManager

* Create a subevent during event creation

* Fix bulk voucher creation

* Introduce subevent.active

* Copy from for subevents

* Show active in list

* ICal download for subevents

* Check start and end of presale

* Failing tests / show cart logic

* Test

* Rebase migrations

* REST API integration of sub-events

* Integrate quota calculation into the traditional quota form

* Make subevent argument to add_position optional

* Log-display foo

* pretixdroid and subevents

* Filter by subevent

* Add more tests

* Some mor tests

* Rebase fixes

* More tests

* Relative dates

* Restrict selection in relative datetime widgets

* Filter subevent list

* Re-label has_subevents

* Rebase fixes, subevents in calendar view

* Performance and caching issues

* Refactor calendar templates

* Permission tests

* Calendar fixes and month selection

* subevent selection

* Rename subevents to dates

* Add tests for calendar views
This commit is contained in:
Raphael Michel
2017-07-11 13:56:00 +02:00
committed by GitHub
parent 554800c06f
commit 8123effa65
141 changed files with 5920 additions and 1012 deletions

View File

@@ -29,12 +29,32 @@
The code tells the app all it needs about your event.
{% endblocktrans %}
</p>
<div id="qrcodeCanvas"></div>
<a href="?flush_key=1" class="btn btn-default">{% trans "Reset authentication token" %}</a>
<script type="text/json" id="qrdata">
{{ qrdata|safe }}
{% if request.event.has_subevents %}
<form class="form-inline helper-display-inline" action="" method="get">
<p>
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "Choose date" context "subevent" %}</option>
{% for se in request.event.subevents.all %}
<option value="{{ se.id }}"
{% if request.GET.subevent|add:0 == se.id %}selected="selected"{% endif %}>
{{ se.name }} {{ se.get_date_range_display }}
</option>
{% endfor %}
</select>
{% endif %}
<button class="btn btn-primary" type="submit">{% trans "Show configuration" %}</button>
</p>
</form>
{% endif %}
{% if not request.event.has_subevents or subevent %}
<div id="qrcodeCanvas"></div>
<a href="?flush_key=1" class="btn btn-default">{% trans "Reset authentication token" %}</a>
<script type="text/json" id="qrdata">
{{ qrdata|safe }}
</script>
</script>
{% endif %}
<script type="text/javascript" src="{% static "pretixplugins/pretixdroid/pretixdroid.js" %}"></script>
{% endblock %}

View File

@@ -1,16 +1,22 @@
from django.conf.urls import url
from django.conf.urls import include, url
from . import views
pretixdroid_api_patterns = [
url(r'^redeem/', views.ApiRedeemView.as_view(),
name='api.redeem'),
url(r'^search/', views.ApiSearchView.as_view(),
name='api.search'),
url(r'^download/', views.ApiDownloadView.as_view(),
name='api.download'),
url(r'^status/', views.ApiStatusView.as_view(),
name='api.status'),
]
urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pretixdroid/', views.ConfigView.as_view(),
name='config'),
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/redeem/', views.ApiRedeemView.as_view(),
name='api.redeem'),
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/search/', views.ApiSearchView.as_view(),
name='api.search'),
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/download/', views.ApiDownloadView.as_view(),
name='api.download'),
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/status/', views.ApiStatusView.as_view(),
name='api.status'),
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/(?P<subevent>\d+)/',
include(pretixdroid_api_patterns)),
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include(pretixdroid_api_patterns)),
]

View File

@@ -8,6 +8,7 @@ from django.db.models import Count, Q
from django.http import (
HttpResponseForbidden, HttpResponseNotFound, JsonResponse,
)
from django.shortcuts import get_object_or_404
from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator
from django.utils.timezone import now
@@ -15,6 +16,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView, View
from pretix.base.models import Checkin, Event, Order, OrderPosition
from pretix.base.models.event import SubEvent
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.urls import build_absolute_uri
from pretix.multidomain.urlreverse import (
@@ -37,13 +39,26 @@ class ConfigView(EventPermissionRequiredMixin, TemplateView):
allowed_chars=string.ascii_uppercase + string.ascii_lowercase + string.digits)
self.request.event.settings.set('pretixdroid_key', key)
subevent = None
url = build_absolute_uri('plugins:pretixdroid:api.redeem', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug
})
if self.request.event.has_subevents:
if self.request.GET.get('subevent'):
subevent = get_object_or_404(SubEvent, event=self.request.event, pk=self.request.GET['subevent'])
url = build_absolute_uri('plugins:pretixdroid:api.redeem', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'subevent': subevent.pk
})
ctx['subevent'] = subevent
ctx['qrdata'] = json.dumps({
'version': API_VERSION,
'url': build_absolute_uri('plugins:pretixdroid:api.redeem', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug
})[:-7], # the slice removes the redeem/ part at the end
'key': key
'url': url[:-7], # the slice removes the redeem/ part at the end
'key': key,
})
return ctx
@@ -61,9 +76,19 @@ class ApiView(View):
return HttpResponseNotFound('Unknown event')
if (not self.event.settings.get('pretixdroid_key')
or self.event.settings.get('pretixdroid_key') != request.GET.get('key', '')):
or self.event.settings.get('pretixdroid_key') != request.GET.get('key', '-unset-')):
return HttpResponseForbidden('Invalid key')
self.subevent = None
if self.event.has_subevents:
if 'subevent' in kwargs:
self.subevent = get_object_or_404(SubEvent, event=self.event, pk=kwargs['subevent'])
else:
return HttpResponseForbidden('No subevent selected.')
else:
if 'subevent' in kwargs:
return HttpResponseForbidden('Subevents not enabled.')
return super().dispatch(request, **kwargs)
@@ -85,7 +110,7 @@ class ApiRedeemView(ApiView):
with transaction.atomic():
created = False
op = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').get(
order__event=self.event, secret=secret
order__event=self.event, secret=secret, subevent=self.subevent
)
if op.order.status == Order.STATUS_PAID or force:
ci, created = Checkin.objects.get_or_create(position=op, defaults={
@@ -161,6 +186,7 @@ class ApiSearchView(ApiView):
& Q(
Q(secret__istartswith=query) | Q(attendee_name__icontains=query) | Q(order__code__istartswith=query)
)
& Q(subevent=self.subevent)
).annotate(checkin_cnt=Count('checkins'))[:25]
response['results'] = [serialize_op(op) for op in ops]
@@ -177,7 +203,7 @@ class ApiDownloadView(ApiView):
}
ops = OrderPosition.objects.select_related('item', 'variation', 'order', 'addon_to').filter(
Q(order__event=self.event)
Q(order__event=self.event) & Q(subevent=self.subevent)
).annotate(checkin_cnt=Count('checkins'))
response['results'] = [serialize_op(op) for op in ops]
@@ -186,25 +212,27 @@ class ApiDownloadView(ApiView):
class ApiStatusView(ApiView):
def get(self, request, **kwargs):
ev = self.subevent or self.event
response = {
'version': API_VERSION,
'event': {
'name': str(self.event),
'name': str(ev.name),
'slug': self.event.slug,
'organizer': {
'name': str(self.event.organizer),
'slug': self.event.organizer.slug
},
'date_from': self.event.date_from,
'date_to': self.event.date_to,
'subevent': self.subevent.pk if self.subevent else str(self.event),
'date_from': ev.date_from,
'date_to': ev.date_to,
'timezone': self.event.settings.timezone,
'url': event_absolute_uri(self.event, 'presale:event.index')
},
'checkins': Checkin.objects.filter(
position__order__event=self.event
position__order__event=self.event, position__subevent=self.subevent
).count(),
'total': OrderPosition.objects.filter(
order__event=self.event, order__status=Order.STATUS_PAID
order__event=self.event, order__status=Order.STATUS_PAID, subevent=self.subevent
).count()
}
@@ -212,28 +240,32 @@ class ApiStatusView(ApiView):
p['item']: p['cnt']
for p in OrderPosition.objects.filter(
order__event=self.event,
order__status=Order.STATUS_PAID
order__status=Order.STATUS_PAID,
subevent=self.subevent
).order_by().values('item').annotate(cnt=Count('id'))
}
op_by_variation = {
p['variation']: p['cnt']
for p in OrderPosition.objects.filter(
order__event=self.event,
order__status=Order.STATUS_PAID
order__status=Order.STATUS_PAID,
subevent=self.subevent
).order_by().values('variation').annotate(cnt=Count('id'))
}
c_by_item = {
p['position__item']: p['cnt']
for p in Checkin.objects.filter(
position__order__event=self.event,
position__order__status=Order.STATUS_PAID
position__order__status=Order.STATUS_PAID,
position__subevent=self.subevent
).order_by().values('position__item').annotate(cnt=Count('id'))
}
c_by_variation = {
p['position__variation']: p['cnt']
for p in Checkin.objects.filter(
position__order__event=self.event,
position__order__status=Order.STATUS_PAID
position__order__status=Order.STATUS_PAID,
position__subevent=self.subevent
).order_by().values('position__variation').annotate(cnt=Count('id'))
}