mirror of
https://github.com/pretix/pretix.git
synced 2026-08-22 12:42:00 +00:00
* added event ical download feature * handle event settings and timezone * add test cases for ical download * fix failed test case for timezone settings * using vobject lib to generate ical * customised UID & add vobject dependency
This commit is contained in:
@@ -68,9 +68,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if frontpage_text %}
|
<div>
|
||||||
{{ frontpage_text|rich_text }}
|
{% if frontpage_text %}
|
||||||
{% endif %}
|
<div class="pull-left">
|
||||||
|
{{ frontpage_text|rich_text }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="pull-right">
|
||||||
|
<a href="{% eventurl event "presale:event.ical.download" %}" class="btn btn-link">
|
||||||
|
<i class="fa fa-calendar"></i> {% trans "Add to Calendar" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% eventsignal event "pretix.presale.signals.front_page_top" %}
|
{% eventsignal event "pretix.presale.signals.front_page_top" %}
|
||||||
{% if event.presale_is_running or event.settings.show_items_outside_presale_period %}
|
{% if event.presale_is_running or event.settings.show_items_outside_presale_period %}
|
||||||
<form method="post" data-asynctask
|
<form method="post" data-asynctask
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ event_patterns = [
|
|||||||
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/invoice/(?P<invoice>[0-9]+)$',
|
url(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/invoice/(?P<invoice>[0-9]+)$',
|
||||||
pretix.presale.views.order.InvoiceDownload.as_view(),
|
pretix.presale.views.order.InvoiceDownload.as_view(),
|
||||||
name='event.invoice.download'),
|
name='event.invoice.download'),
|
||||||
|
url(r'^ical$',
|
||||||
|
pretix.presale.views.event.EventIcalDownload.as_view(),
|
||||||
|
name='event.ical.download'),
|
||||||
url(r'^auth/$', pretix.presale.views.event.EventAuth.as_view(), name='event.auth'),
|
url(r'^auth/$', pretix.presale.views.event.EventAuth.as_view(), name='event.auth'),
|
||||||
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
|
url(r'^$', pretix.presale.views.event.EventIndex.as_view(), name='event.index'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import sys
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
import vobject
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db.models import Count, Prefetch, Q
|
from django.db.models import Count, Prefetch, Q
|
||||||
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
from pytz import timezone
|
||||||
|
|
||||||
from pretix.base.models import ItemVariation
|
from pretix.base.models import ItemVariation
|
||||||
from pretix.multidomain.urlreverse import eventreverse
|
from pretix.multidomain.urlreverse import eventreverse
|
||||||
@@ -105,6 +111,48 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class EventIcalDownload(EventViewMixin, View):
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def event_timezone(self):
|
||||||
|
return timezone(self.request.event.settings.timezone)
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
if not self.request.event:
|
||||||
|
raise Http404(_('Unknown event code or not authorized to access this event.'))
|
||||||
|
|
||||||
|
event = self.request.event
|
||||||
|
creation_time = datetime.now(pytz.utc)
|
||||||
|
cal = vobject.iCalendar()
|
||||||
|
cal.add('prodid').value = '-//pretix//{}//'.format(settings.PRETIX_INSTANCE_NAME)
|
||||||
|
|
||||||
|
vevent = cal.add('vevent')
|
||||||
|
vevent.add('summary').value = str(event.name)
|
||||||
|
vevent.add('dtstamp').value = creation_time
|
||||||
|
vevent.add('location').value = str(event.location)
|
||||||
|
vevent.add('organizer').value = event.organizer.name
|
||||||
|
vevent.add('uid').value = '{}-{}-{}'.format(
|
||||||
|
event.organizer.slug, event.slug, creation_time.strftime('%Y%m%d%H%M%S%f')
|
||||||
|
)
|
||||||
|
|
||||||
|
if event.settings.show_times:
|
||||||
|
vevent.add('dtstart').value = event.date_from.astimezone(self.event_timezone)
|
||||||
|
else:
|
||||||
|
vevent.add('dtstart').value = event.date_from.astimezone(self.event_timezone).date()
|
||||||
|
|
||||||
|
if event.settings.show_date_to:
|
||||||
|
if event.settings.show_times:
|
||||||
|
vevent.add('dtend').value = event.date_to.astimezone(self.event_timezone)
|
||||||
|
else:
|
||||||
|
vevent.add('dtend').value = event.date_to.astimezone(self.event_timezone).date()
|
||||||
|
|
||||||
|
resp = HttpResponse(cal.serialize(), content_type='text/calendar')
|
||||||
|
resp['Content-Disposition'] = 'attachment; filename="{}-{}.ics"'.format(
|
||||||
|
event.organizer.slug, event.slug
|
||||||
|
)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
class EventAuth(View):
|
class EventAuth(View):
|
||||||
@method_decorator(csrf_exempt)
|
@method_decorator(csrf_exempt)
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
|||||||
@@ -36,3 +36,4 @@ pycparser==2.13 # https://github.com/eliben/pycparser/issues/147
|
|||||||
# Banktransfer
|
# Banktransfer
|
||||||
chardet>=2.3,<3
|
chardet>=2.3,<3
|
||||||
mt-940==3.2
|
mt-940==3.2
|
||||||
|
vobject==0.9.*
|
||||||
|
|||||||
+2
-1
@@ -97,7 +97,8 @@ setup(
|
|||||||
'stripe==1.22.*',
|
'stripe==1.22.*',
|
||||||
'chardet>=2.3,<3',
|
'chardet>=2.3,<3',
|
||||||
'mt-940==3.2',
|
'mt-940==3.2',
|
||||||
'django-i18nfield'
|
'django-i18nfield',
|
||||||
|
'vobject==0.9.*'
|
||||||
],
|
],
|
||||||
extras_require={
|
extras_require={
|
||||||
'dev': [
|
'dev': [
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import re
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from pytz import timezone
|
||||||
from tests.base import SoupTest
|
from tests.base import SoupTest
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -49,6 +52,16 @@ class EventMiddlewareTest(EventTestMixin, SoupTest):
|
|||||||
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
|
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
def test_not_found_event(self):
|
||||||
|
resp = self.client.get('/%s/%s/ical' % ('foo', 'bar'))
|
||||||
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
|
def test_mandatory_field(self):
|
||||||
|
self.event.date_to = self.event.date_from + datetime.timedelta(days=2)
|
||||||
|
self.event.save()
|
||||||
|
resp = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug))
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
|
||||||
class ItemDisplayTest(EventTestMixin, SoupTest):
|
class ItemDisplayTest(EventTestMixin, SoupTest):
|
||||||
def test_not_active(self):
|
def test_not_active(self):
|
||||||
@@ -548,6 +561,115 @@ class TestResendLink(EventTestMixin, SoupTest):
|
|||||||
self.assertIn('DUMMY2', mail.outbox[0].body)
|
self.assertIn('DUMMY2', mail.outbox[0].body)
|
||||||
|
|
||||||
|
|
||||||
|
class EventIcalDownloadTest(EventTestMixin, SoupTest):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.event.settings.show_date_to = True
|
||||||
|
self.event.settings.show_times = True
|
||||||
|
self.event.location = 'DUMMY ARENA'
|
||||||
|
self.event.date_from = datetime.datetime(2013, 12, 26, 21, 57, 58, tzinfo=datetime.timezone.utc)
|
||||||
|
self.event.date_to = self.event.date_from + datetime.timedelta(days=2)
|
||||||
|
self.event.settings.timezone = 'UTC'
|
||||||
|
self.event.save()
|
||||||
|
|
||||||
|
def test_response_type(self):
|
||||||
|
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug))
|
||||||
|
self.assertEqual(ical['Content-Type'], 'text/calendar')
|
||||||
|
self.assertEqual(ical['Content-Disposition'], 'attachment; filename="{}-{}.ics"'.format(
|
||||||
|
self.orga.slug, self.event.slug
|
||||||
|
))
|
||||||
|
|
||||||
|
def test_header_footer(self):
|
||||||
|
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug)).content.decode()
|
||||||
|
self.assertTrue(ical.startswith('BEGIN:VCALENDAR'), 'missing VCALENDAR header')
|
||||||
|
self.assertTrue(ical.strip().endswith('END:VCALENDAR'), 'missing VCALENDAR footer')
|
||||||
|
self.assertIn('BEGIN:VEVENT', ical, 'missing VEVENT header')
|
||||||
|
self.assertIn('END:VEVENT', ical, 'missing VEVENT footer')
|
||||||
|
|
||||||
|
def test_timezone_header_footer(self):
|
||||||
|
self.event.settings.timezone = 'Asia/Tokyo'
|
||||||
|
self.event.save()
|
||||||
|
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug)).content.decode()
|
||||||
|
self.assertTrue(ical.startswith('BEGIN:VCALENDAR'), 'missing VCALENDAR header')
|
||||||
|
self.assertTrue(ical.strip().endswith('END:VCALENDAR'), 'missing VCALENDAR footer')
|
||||||
|
self.assertIn('BEGIN:VEVENT', ical, 'missing VEVENT header')
|
||||||
|
self.assertIn('END:VEVENT', ical, 'missing VEVENT footer')
|
||||||
|
self.assertIn('BEGIN:VTIMEZONE', ical, 'missing VTIMEZONE header')
|
||||||
|
self.assertIn('END:VTIMEZONE', ical, 'missing VTIMEZONE footer')
|
||||||
|
|
||||||
|
def test_metadata(self):
|
||||||
|
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug)).content.decode()
|
||||||
|
self.assertIn('VERSION:2.0', ical, 'incorrect version tag - 2.0')
|
||||||
|
self.assertIn('-//pretix//%s//' % settings.PRETIX_INSTANCE_NAME, ical, 'incorrect PRODID')
|
||||||
|
|
||||||
|
def test_event_info(self):
|
||||||
|
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug)).content.decode()
|
||||||
|
self.assertIn('SUMMARY:%s' % self.event.name, ical, 'incorrect correct summary')
|
||||||
|
self.assertIn('LOCATION:DUMMY ARENA', ical, 'incorrect location')
|
||||||
|
self.assertIn('ORGANIZER:%s' % self.event.organizer.name, ical, 'incorrect organizer')
|
||||||
|
self.assertTrue(re.search(r'DTSTAMP:\d{8}T\d{6}Z', ical), 'incorrect timestamp')
|
||||||
|
self.assertTrue(re.search(r'UID:\w*-\w*-\d{20}', ical), 'missing UID key')
|
||||||
|
|
||||||
|
def test_utc_timezone(self):
|
||||||
|
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug)).content.decode()
|
||||||
|
# according to icalendar spec, timezone must NOT be shown if it is UTC
|
||||||
|
self.assertIn('DTSTART:%s' % self.event.date_from.strftime('%Y%m%dT%H%M%SZ'), ical, 'incorrect start time')
|
||||||
|
self.assertIn('DTEND:%s' % self.event.date_to.strftime('%Y%m%dT%H%M%SZ'), ical, 'incorrect end time')
|
||||||
|
|
||||||
|
def test_include_timezone(self):
|
||||||
|
self.event.settings.timezone = 'Asia/Tokyo'
|
||||||
|
self.event.save()
|
||||||
|
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug)).content.decode()
|
||||||
|
# according to icalendar spec, timezone must be shown if it is not UTC
|
||||||
|
fmt = '%Y%m%dT%H%M%S'
|
||||||
|
self.assertIn('DTSTART;TZID=%s:%s' %
|
||||||
|
(self.event.settings.timezone,
|
||||||
|
self.event.date_from.astimezone(timezone(self.event.settings.timezone)).strftime(fmt)),
|
||||||
|
ical, 'incorrect start time')
|
||||||
|
self.assertIn('DTEND;TZID=%s:%s' %
|
||||||
|
(self.event.settings.timezone,
|
||||||
|
self.event.date_to.astimezone(timezone(self.event.settings.timezone)).strftime(fmt)),
|
||||||
|
ical, 'incorrect end time')
|
||||||
|
self.assertIn('TZID:%s' % self.event.settings.timezone, ical, 'missing VCALENDAR')
|
||||||
|
|
||||||
|
def test_no_time(self):
|
||||||
|
self.event.settings.show_times = False
|
||||||
|
self.event.save()
|
||||||
|
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug)).content.decode()
|
||||||
|
self.assertIn('DTSTART;VALUE=DATE:%s' % self.event.date_from.strftime('%Y%m%d'), ical, 'incorrect start date')
|
||||||
|
self.assertIn('DTEND;VALUE=DATE:%s' % self.event.date_to.strftime('%Y%m%d'), ical, 'incorrect end date')
|
||||||
|
|
||||||
|
def test_no_date_to(self):
|
||||||
|
self.event.settings.timezone = 'Asia/Tokyo'
|
||||||
|
self.event.settings.show_date_to = False
|
||||||
|
self.event.save()
|
||||||
|
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug)).content.decode()
|
||||||
|
fmt = '%Y%m%dT%H%M%S'
|
||||||
|
self.assertIn('DTSTART;TZID=%s:%s' %
|
||||||
|
(self.event.settings.timezone,
|
||||||
|
self.event.date_from.astimezone(timezone(self.event.settings.timezone)).strftime(fmt)),
|
||||||
|
ical, 'incorrect start time')
|
||||||
|
self.assertNotIn('DTEND', ical, 'unexpected end time attribute')
|
||||||
|
|
||||||
|
def test_no_date_to_and_time(self):
|
||||||
|
self.event.settings.show_date_to = False
|
||||||
|
self.event.settings.show_times = False
|
||||||
|
self.event.save()
|
||||||
|
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug)).content.decode()
|
||||||
|
self.assertIn('DTSTART;VALUE=DATE:%s' % self.event.date_from.strftime('%Y%m%d'), ical, 'incorrect start date')
|
||||||
|
self.assertNotIn('DTEND', ical, 'unexpected end time attribute')
|
||||||
|
|
||||||
|
def test_local_date_diff_from_utc(self):
|
||||||
|
self.event.date_from = datetime.datetime(2013, 12, 26, 21, 57, 58, tzinfo=datetime.timezone.utc)
|
||||||
|
self.event.date_to = self.event.date_from + datetime.timedelta(days=2)
|
||||||
|
self.event.settings.timezone = 'Asia/Tokyo'
|
||||||
|
self.event.settings.show_times = False
|
||||||
|
self.event.save()
|
||||||
|
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug)).content.decode()
|
||||||
|
self.assertIn('DTSTART;VALUE=DATE:20131227', ical, 'incorrect start date')
|
||||||
|
self.assertIn('DTEND;VALUE=DATE:20131229', ical, 'incorrect end date')
|
||||||
|
|
||||||
|
|
||||||
class EventSlugBlacklistValidatorTest(EventTestMixin, SoupTest):
|
class EventSlugBlacklistValidatorTest(EventTestMixin, SoupTest):
|
||||||
def test_slug_validation(self):
|
def test_slug_validation(self):
|
||||||
event = Event(
|
event = Event(
|
||||||
|
|||||||
Reference in New Issue
Block a user