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

@@ -14,6 +14,7 @@ from pretix.base.models import (
Event, Item, ItemCategory, ItemVariation, Order, Organizer, Quota, Team,
User, WaitingListEntry,
)
from pretix.base.models.items import SubEventItem, SubEventItemVariation
class EventTestMixin:
@@ -130,6 +131,94 @@ class ItemDisplayTest(EventTestMixin, SoupTest):
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertNotIn("Early-bird", resp.rendered_content)
def test_subevents_inactive_unknown(self):
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name='Foo', date_from=now(), active=False)
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se1.pk))
assert resp.status_code == 404
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se1.pk + 1000))
assert resp.status_code == 404
def test_subevent_list(self):
self.event.has_subevents = True
self.event.save()
self.event.subevents.create(name='Foo SE1', date_from=now() + datetime.timedelta(days=1), active=True)
self.event.subevents.create(name='Foo SE2', date_from=now() + datetime.timedelta(days=1), active=False)
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertIn("Foo SE1", resp.rendered_content)
self.assertNotIn("Foo SE2", resp.rendered_content)
def test_subevent_calendar(self):
self.event.settings.event_list_type = 'calendar'
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name='Foo SE1', date_from=now() + datetime.timedelta(days=64), active=True)
self.event.subevents.create(name='Foo SE2', date_from=now() + datetime.timedelta(days=32), active=True)
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertIn("Foo SE2", resp.rendered_content)
self.assertNotIn("Foo SE1", resp.rendered_content)
resp = self.client.get('/%s/%s/?year=%d&month=%d' % (self.orga.slug, self.event.slug, se1.date_from.year,
se1.date_from.month))
self.assertIn("Foo SE1", resp.rendered_content)
self.assertNotIn("Foo SE2", resp.rendered_content)
def test_subevents(self):
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se2 = self.event.subevents.create(name='Foo', date_from=now(), active=True)
q = Quota.objects.create(event=self.event, name='Quota', size=2, subevent=se1)
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0)
q.items.add(item)
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se1.pk))
self.assertIn("Early-bird", resp.rendered_content)
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se2.pk))
self.assertNotIn("Early-bird", resp.rendered_content)
def test_subevent_prices(self):
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se2 = self.event.subevents.create(name='Foo', date_from=now(), active=True)
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=15)
q = Quota.objects.create(event=self.event, name='Quota', size=2, subevent=se1)
q.items.add(item)
q = Quota.objects.create(event=self.event, name='Quota', size=2, subevent=se2)
q.items.add(item)
SubEventItem.objects.create(subevent=se1, item=item, price=12)
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se1.pk))
self.assertIn("12.00", resp.rendered_content)
self.assertNotIn("15.00", resp.rendered_content)
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se2.pk))
self.assertIn("15.00", resp.rendered_content)
self.assertNotIn("12.00", resp.rendered_content)
def test_subevent_net_prices(self):
self.event.has_subevents = True
self.event.save()
self.event.settings.display_net_prices = True
se1 = self.event.subevents.create(name='Foo', date_from=now(), active=True)
se2 = self.event.subevents.create(name='Foo', date_from=now(), active=True)
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=15,
tax_rate=19)
q = Quota.objects.create(event=self.event, name='Quota', size=2, subevent=se1)
q.items.add(item)
q = Quota.objects.create(event=self.event, name='Quota', size=2, subevent=se2)
q.items.add(item)
SubEventItem.objects.create(subevent=se1, item=item, price=12)
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se1.pk))
self.assertIn("10.08", resp.rendered_content)
self.assertNotIn("12.00", resp.rendered_content)
self.assertNotIn("15.00", resp.rendered_content)
resp = self.client.get('/%s/%s/%d/' % (self.orga.slug, self.event.slug, se2.pk))
self.assertIn("12.61", resp.rendered_content)
self.assertNotIn("12.00", resp.rendered_content)
self.assertNotIn("15.00", resp.rendered_content)
def test_no_variations_in_quota(self):
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
q = Quota.objects.create(event=self.event, name='Quota', size=2)
@@ -357,6 +446,99 @@ class VoucherRedeemItemDisplayTest(EventTestMixin, SoupTest):
html = self.client.get('/%s/%s/redeem?voucher=%s' % (self.orga.slug, self.event.slug, 'ABC'), follow=True)
assert "alert-danger" in html.rendered_content
def test_subevent_net_prices(self):
self.event.settings.display_net_prices = True
self.event.has_subevents = True
self.event.save()
self.item.tax_rate = 19
self.item.save()
se1 = self.event.subevents.create(name='SE1', date_from=now(), active=True)
q = Quota.objects.create(event=self.event, name='Quota', size=2, subevent=se1)
var1 = ItemVariation.objects.create(item=self.item, value='Red', position=1)
var2 = ItemVariation.objects.create(item=self.item, value='Black', position=2)
q.variations.add(var1)
q.variations.add(var2)
SubEventItemVariation.objects.create(subevent=se1, variation=var1, price=10)
self.v.value = Decimal("2.00")
self.v.price_mode = 'subtract'
self.v.save()
html = self.client.get('/%s/%s/redeem?voucher=%s&subevent=%s' % (
self.orga.slug, self.event.slug, self.v.code, se1.pk
))
assert "SE1" in html.rendered_content
assert "Early-bird" in html.rendered_content
assert "8.40" in html.rendered_content
assert "6.72" in html.rendered_content
def test_subevent_prices(self):
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name='SE1', date_from=now(), active=True)
q = Quota.objects.create(event=self.event, name='Quota', size=2, subevent=se1)
var1 = ItemVariation.objects.create(item=self.item, value='Red', position=1)
var2 = ItemVariation.objects.create(item=self.item, value='Black', position=2)
q.variations.add(var1)
q.variations.add(var2)
SubEventItemVariation.objects.create(subevent=se1, variation=var1, price=10)
self.v.value = Decimal("2.00")
self.v.price_mode = 'subtract'
self.v.save()
html = self.client.get('/%s/%s/redeem?voucher=%s&subevent=%s' % (
self.orga.slug, self.event.slug, self.v.code, se1.pk
))
assert "SE1" in html.rendered_content
assert "Early-bird" in html.rendered_content
assert "10.00" in html.rendered_content
assert "8.00" in html.rendered_content
assert "variation_%d_%d" % (self.item.pk, var1.pk) in html.rendered_content
assert "variation_%d_%d" % (self.item.pk, var2.pk) in html.rendered_content
def test_voucher_ignore_other_subevent(self):
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name='SE1', date_from=now(), active=True)
se2 = self.event.subevents.create(name='SE2', date_from=now(), active=True)
q = Quota.objects.create(event=self.event, name='Quota', size=2, subevent=se1)
var1 = ItemVariation.objects.create(item=self.item, value='Red', position=1)
var2 = ItemVariation.objects.create(item=self.item, value='Black', position=2)
q.variations.add(var1)
q.variations.add(var2)
self.v.subevent = se1
self.v.save()
html = self.client.get('/%s/%s/redeem?voucher=%s&subevent=%s' % (
self.orga.slug, self.event.slug, self.v.code, se2.pk
))
assert "SE1" in html.rendered_content
def test_voucher_quota(self):
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name='SE1', date_from=now(), active=True)
se2 = self.event.subevents.create(name='SE2', date_from=now(), active=True)
q = Quota.objects.create(event=self.event, name='Quota', size=0, subevent=se1)
q2 = Quota.objects.create(event=self.event, name='Quota', size=2, subevent=se2)
var1 = ItemVariation.objects.create(item=self.item, value='Red', position=1)
var2 = ItemVariation.objects.create(item=self.item, value='Black', position=2)
q.variations.add(var1)
q2.variations.add(var1)
q.variations.add(var2)
q2.variations.add(var1)
self.v.save()
html = self.client.get('/%s/%s/redeem?voucher=%s&subevent=%s' % (
self.orga.slug, self.event.slug, self.v.code, se1.pk
))
assert "SE1" in html.rendered_content
assert "variation_%d_%d" % (self.item.pk, var1.pk) not in html.rendered_content
assert "variation_%d_%d" % (self.item.pk, var2.pk) not in html.rendered_content
class WaitingListTest(EventTestMixin, SoupTest):
def setUp(self):
@@ -376,7 +558,7 @@ class WaitingListTest(EventTestMixin, SoupTest):
self.assertEqual(response.status_code, 200)
self.assertNotIn('waitinglist', response.rendered_content)
response = self.client.get(
'/%s/%s/waitinglist?item=%d' % (self.orga.slug, self.event.slug, self.item.pk + 1)
'/%s/%s/waitinglist/?item=%d' % (self.orga.slug, self.event.slug, self.item.pk + 1)
)
self.assertEqual(response.status_code, 302)
@@ -389,12 +571,12 @@ class WaitingListTest(EventTestMixin, SoupTest):
def test_submit_form(self):
response = self.client.get(
'/%s/%s/waitinglist?item=%d' % (self.orga.slug, self.event.slug, self.item.pk)
'/%s/%s/waitinglist/?item=%d' % (self.orga.slug, self.event.slug, self.item.pk)
)
self.assertEqual(response.status_code, 200)
self.assertIn('waiting list', response.rendered_content)
response = self.client.post(
'/%s/%s/waitinglist?item=%d' % (self.orga.slug, self.event.slug, self.item.pk), {
'/%s/%s/waitinglist/?item=%d' % (self.orga.slug, self.event.slug, self.item.pk), {
'email': 'foo@bar.com'
}
)
@@ -406,17 +588,77 @@ class WaitingListTest(EventTestMixin, SoupTest):
assert wle.voucher is None
assert wle.locale == 'en'
def test_invalid_item(self):
def test_subevent_valid(self):
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name="Foo", date_from=now(), active=True)
se2 = self.event.subevents.create(name="Foobar", date_from=now(), active=True)
self.q.subevent = se1
self.q.save()
q2 = self.event.quotas.create(name="Foobar", size=100, subevent=se2)
q2.items.add(self.item)
response = self.client.get(
'/%s/%s/waitinglist?item=%d' % (self.orga.slug, self.event.slug, self.item.pk + 1)
'/%s/%s/waitinglist/?item=%d&subevent=%d' % (self.orga.slug, self.event.slug, self.item.pk, se1.pk)
)
self.assertEqual(response.status_code, 200)
self.assertIn('waiting list', response.rendered_content)
response = self.client.post(
'/%s/%s/waitinglist/?item=%d&subevent=%d' % (self.orga.slug, self.event.slug, self.item.pk, se1.pk), {
'email': 'foo@bar.com'
}
)
self.assertEqual(response.status_code, 302)
wle = WaitingListEntry.objects.get(email='foo@bar.com')
assert wle.event == self.event
assert wle.item == self.item
assert wle.subevent == se1
def test_invalid_item(self):
response = self.client.get(
'/%s/%s/waitinglist/?item=%d' % (self.orga.slug, self.event.slug, self.item.pk + 1)
)
self.assertEqual(response.status_code, 302)
def test_invalid_subevent(self):
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name="Foo", date_from=now(), active=False)
response = self.client.get(
'/%s/%s/waitinglist/?item=%d' % (self.orga.slug, self.event.slug, self.item.pk)
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
'/%s/%s/waitinglist/?item=%d&subevent=%d' % (self.orga.slug, self.event.slug, self.item.pk, se1.pk + 100)
)
self.assertEqual(response.status_code, 404)
response = self.client.get(
'/%s/%s/waitinglist/?item=%d&subevent=%d' % (self.orga.slug, self.event.slug, self.item.pk, se1.pk)
)
self.assertEqual(response.status_code, 404)
def test_available(self):
self.q.size = 1
self.q.save()
response = self.client.post(
'/%s/%s/waitinglist?item=%d' % (self.orga.slug, self.event.slug, self.item.pk), {
'/%s/%s/waitinglist/?item=%d' % (self.orga.slug, self.event.slug, self.item.pk), {
'email': 'foo@bar.com'
}
)
self.assertEqual(response.status_code, 302)
self.assertFalse(WaitingListEntry.objects.filter(email='foo@bar.com').exists())
def test_subevent_available(self):
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(name="Foo", date_from=now(), active=True)
se2 = self.event.subevents.create(name="Foobar", date_from=now(), active=True)
self.q.size = 1
self.q.subevent = se1
self.q.save()
q2 = self.event.quotas.create(name="Foobar", size=0, subevent=se2)
q2.items.add(self.item)
response = self.client.post(
'/%s/%s/waitinglist/?item=%d&subevent=%d' % (self.orga.slug, self.event.slug, self.item.pk, se1.pk), {
'email': 'foo@bar.com'
}
)
@@ -575,14 +817,14 @@ class EventIcalDownloadTest(EventTestMixin, SoupTest):
self.event.save()
def test_response_type(self):
ical = self.client.get('/%s/%s/ical' % (self.orga.slug, self.event.slug))
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.assertEqual(ical['Content-Disposition'], 'attachment; filename="{}-{}-0.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()
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')
@@ -591,7 +833,7 @@ class EventIcalDownloadTest(EventTestMixin, SoupTest):
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()
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')
@@ -600,20 +842,20 @@ class EventIcalDownloadTest(EventTestMixin, SoupTest):
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()
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()
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')
self.assertTrue(re.search(r'UID:\w*-\w*-0-\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()
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')
@@ -621,7 +863,7 @@ class EventIcalDownloadTest(EventTestMixin, SoupTest):
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()
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' %
@@ -637,7 +879,7 @@ class EventIcalDownloadTest(EventTestMixin, SoupTest):
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()
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')
@@ -645,7 +887,7 @@ class EventIcalDownloadTest(EventTestMixin, SoupTest):
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()
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,
@@ -657,7 +899,7 @@ class EventIcalDownloadTest(EventTestMixin, SoupTest):
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()
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')
@@ -667,10 +909,35 @@ class EventIcalDownloadTest(EventTestMixin, SoupTest):
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()
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')
def test_subevent_required(self):
self.event.has_subevents = True
self.event.save()
resp = self.client.get('/%s/%s/ical/' % (self.orga.slug, self.event.slug))
assert resp.status_code == 404
resp = self.client.get('/%s/%s/ical/100/' % (self.orga.slug, self.event.slug))
assert resp.status_code == 404
def test_subevent(self):
self.event.has_subevents = True
self.event.save()
se1 = self.event.subevents.create(
name='My fancy subevent',
location='Heeeeeere',
date_from=datetime.datetime(2014, 12, 26, 21, 57, 58, tzinfo=datetime.timezone.utc),
date_to=datetime.datetime(2014, 12, 28, 21, 57, 58, tzinfo=datetime.timezone.utc),
active=True
)
self.event.settings.show_times = False
ical = self.client.get('/%s/%s/ical/%d/' % (self.orga.slug, self.event.slug, se1.pk)).content.decode()
self.assertIn('DTSTART;VALUE=DATE:20141226', ical, 'incorrect start date')
self.assertIn('DTEND;VALUE=DATE:20141228', ical, 'incorrect end date')
self.assertIn('SUMMARY:%s' % se1.name, ical, 'incorrect correct summary')
self.assertIn('LOCATION:Heeeeeere', ical, 'incorrect location')
class EventSlugBlacklistValidatorTest(EventTestMixin, SoupTest):
def test_slug_validation(self):