Seat-specific vouchers (#1486)

* Basic functionality

* API

* Do not delete seats with vouchers

* Show seat in list of seats

* Validate availability of seats

* Fix invalid logic in Seat.is_available

* Show voucher name in edit form
This commit is contained in:
Raphael Michel
2019-11-15 10:56:34 +01:00
committed by GitHub
parent f79df47b78
commit a2c1c69d7e
19 changed files with 474 additions and 42 deletions

View File

@@ -8,7 +8,7 @@ from django.utils.timezone import now
from django_scopes import scopes_disabled
from pytz import UTC
from pretix.base.models import Event, Voucher
from pretix.base.models import Event, SeatingPlan, Voucher
@pytest.fixture
@@ -44,7 +44,8 @@ TEST_VOUCHER_RES = {
'tag': 'Foo',
'comment': '',
'show_hidden_items': True,
'subevent': None
'subevent': None,
'seat': None,
}
@@ -1049,3 +1050,223 @@ def test_create_multiple_vouchers_duplicate_code(token_client, organizer, event,
assert resp.data == [{}, {'code': ['Duplicate voucher code in request.']}]
with scopes_disabled():
assert Voucher.objects.count() == 0
@pytest.fixture
def seatingplan(organizer, event):
plan = SeatingPlan.objects.create(
name="Plan", organizer=organizer, layout="{}"
)
event.seating_plan = plan
event.save()
return plan
@pytest.fixture
def seat1(item, event):
return event.seats.create(name="A1", product=item, seat_guid="A1")
@pytest.mark.django_db
def test_create_multiple_vouchers_duplicate_seat(token_client, organizer, event, item, seat1, seatingplan):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/vouchers/batch_create/'.format(organizer.slug, event.slug),
data=[
{
'code': 'ABCDEFGHI',
'max_usages': 1,
'valid_until': None,
'block_quota': False,
'allow_ignore_quota': False,
'price_mode': 'set',
'value': '12.00',
'item': item.pk,
'variation': None,
'quota': None,
'tag': 'Foo',
'comment': '',
'subevent': None,
'seat': 'A1',
},
{
'code': 'ABCDEFGHI',
'max_usages': 1,
'valid_until': None,
'block_quota': True,
'allow_ignore_quota': False,
'price_mode': 'set',
'value': '12.00',
'item': item.pk,
'variation': None,
'quota': None,
'tag': 'Foo',
'comment': '',
'subevent': None,
'seat': 'A1',
}
], format='json'
)
assert resp.status_code == 400
assert resp.data == [{}, {'code': ['Duplicate seat ID in request.']}]
with scopes_disabled():
assert Voucher.objects.count() == 0
@pytest.mark.django_db
def test_set_seat_ok(token_client, organizer, event, seatingplan, seat1, item):
with scopes_disabled():
v = event.vouchers.create(item=item)
change_voucher(
token_client, organizer, event, v,
data={
'seat': 'A1'
},
)
with scopes_disabled():
v.refresh_from_db()
assert v.seat == seat1
@pytest.mark.django_db
def test_save_set_seat(token_client, organizer, event, seatingplan, seat1, item):
with scopes_disabled():
v = event.vouchers.create(item=item, seat=seat1)
change_voucher(
token_client, organizer, event, v,
data={
'seat': 'A1'
},
)
with scopes_disabled():
v.refresh_from_db()
assert v.seat == seat1
@pytest.mark.django_db
def test_set_seat_unknown(token_client, organizer, event, seatingplan, seat1, item):
with scopes_disabled():
v = event.vouchers.create(item=item)
change_voucher(
token_client, organizer, event, v,
data={
'seat': 'unknown'
},
expected_failure=True
)
@pytest.mark.django_db
def test_seat_seat_productmissing(token_client, organizer, event, seatingplan, seat1, item, quota):
with scopes_disabled():
v = event.vouchers.create(quota=quota)
change_voucher(
token_client, organizer, event, v,
data={
'seat': 'A1'
},
expected_failure=True
)
@pytest.mark.django_db
def test_seat_seat_productwrong(token_client, organizer, event, seatingplan, seat1, item, quota):
with scopes_disabled():
i2 = event.items.create(name="Budget Ticket", default_price=23)
v = event.vouchers.create(item=i2)
change_voucher(
token_client, organizer, event, v,
data={
'seat': 'A1'
},
expected_failure=True
)
@pytest.mark.django_db
def test_seat_seat_usages(token_client, organizer, event, seatingplan, seat1, item, quota):
with scopes_disabled():
v = event.vouchers.create(item=item, max_usages=2)
change_voucher(
token_client, organizer, event, v,
data={
'seat': 'A1'
},
expected_failure=True
)
@pytest.mark.django_db
def test_seat_seat_duplicate(token_client, organizer, event, seatingplan, seat1, item, quota):
with scopes_disabled():
event.vouchers.create(item=item, seat=seat1)
v = event.vouchers.create(item=item)
change_voucher(
token_client, organizer, event, v,
data={
'seat': 'A1'
},
expected_failure=True
)
@pytest.mark.django_db
def test_set_seat_subevent(token_client, organizer, event, seatingplan, seat1, item, quota):
with scopes_disabled():
event.has_subevents = True
event.save()
se1 = event.subevents.create(name="Foobar", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
se2 = event.subevents.create(name="Baz", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
seat1 = event.seats.create(name="A1", product=item, seat_guid="A1", subevent=se1)
event.seats.create(name="A1", product=item, seat_guid="A1", subevent=se2)
v = event.vouchers.create(item=item)
change_voucher(
token_client, organizer, event, v,
data={
'seat': 'A1',
'subevent': se1.pk
},
)
with scopes_disabled():
v.refresh_from_db()
assert v.seat == seat1
assert v.subevent == se1
@pytest.mark.django_db
def test_set_seat_subevent_required(token_client, organizer, event, seatingplan, seat1, item, quota):
with scopes_disabled():
event.has_subevents = True
event.save()
se1 = event.subevents.create(name="Foobar", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
se2 = event.subevents.create(name="Baz", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
seat1 = event.seats.create(name="A1", product=item, seat_guid="A1", subevent=se1)
event.seats.create(name="A1", product=item, seat_guid="A1", subevent=se2)
event.vouchers.create(item=item, seat=seat1)
v = event.vouchers.create(item=item)
change_voucher(
token_client, organizer, event, v,
data={
'seat': 'A1',
},
expected_failure=True
)
@pytest.mark.django_db
def test_set_seat_subevent_invalid(token_client, organizer, event, seatingplan, seat1, item, quota):
with scopes_disabled():
event.has_subevents = True
event.save()
se1 = event.subevents.create(name="Foobar", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
se2 = event.subevents.create(name="Baz", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
seat1 = event.seats.create(name="A1", product=item, seat_guid="A1", subevent=se1)
event.seats.create(name="B1", product=item, seat_guid="B1", subevent=se2)
event.vouchers.create(item=item, seat=seat1, subevent=se2)
v = event.vouchers.create(item=item)
change_voucher(
token_client, organizer, event, v,
data={
'seat': 'A1',
},
expected_failure=True
)

View File

@@ -2020,7 +2020,7 @@ class SeatingTestCase(TestCase):
@classscope(attr='organizer')
def test_free(self):
assert set(self.event.free_seats) == {self.seat_a1, self.seat_a2}
assert set(self.event.free_seats()) == {self.seat_a1, self.seat_a2}
assert self.seat_a1.is_available()
assert self.seat_a2.is_available()
@@ -2028,7 +2028,7 @@ class SeatingTestCase(TestCase):
def test_blocked(self):
self.seat_a1.blocked = True
self.seat_a1.save()
assert set(self.event.free_seats) == {self.seat_a2}
assert set(self.event.free_seats()) == {self.seat_a2}
assert not self.seat_a1.is_available()
assert self.seat_a2.is_available()
@@ -2043,7 +2043,7 @@ class SeatingTestCase(TestCase):
order=o, item=self.ticket, variation=None, price=Decimal("12"),
seat=self.seat_a1
)
assert set(self.event.free_seats) == {self.seat_a2}
assert set(self.event.free_seats()) == {self.seat_a2}
assert not self.seat_a1.is_available()
@classscope(attr='organizer')
@@ -2057,7 +2057,7 @@ class SeatingTestCase(TestCase):
order=o, item=self.ticket, variation=None, price=Decimal("12"),
seat=self.seat_a1
)
assert set(self.event.free_seats) == {self.seat_a2}
assert set(self.event.free_seats()) == {self.seat_a2}
assert not self.seat_a1.is_available()
@classscope(attr='organizer')
@@ -2071,7 +2071,7 @@ class SeatingTestCase(TestCase):
order=o, item=self.ticket, variation=None, price=Decimal("12"),
seat=self.seat_a1
)
assert set(self.event.free_seats) == {self.seat_a1, self.seat_a2}
assert set(self.event.free_seats()) == {self.seat_a1, self.seat_a2}
assert self.seat_a1.is_available()
@classscope(attr='organizer')
@@ -2080,7 +2080,7 @@ class SeatingTestCase(TestCase):
event=self.event, cart_id='a', item=self.ticket, seat=self.seat_a1,
price=23, expires=now() + timedelta(minutes=10)
)
assert set(self.event.free_seats) == {self.seat_a2}
assert set(self.event.free_seats()) == {self.seat_a2}
assert not self.seat_a1.is_available()
@classscope(attr='organizer')
@@ -2089,7 +2089,7 @@ class SeatingTestCase(TestCase):
event=self.event, cart_id='a', item=self.ticket, seat=self.seat_a1,
price=23, expires=now() - timedelta(minutes=10)
)
assert set(self.event.free_seats) == {self.seat_a1, self.seat_a2}
assert set(self.event.free_seats()) == {self.seat_a1, self.seat_a2}
assert self.seat_a1.is_available()
@classscope(attr='organizer')
@@ -2106,7 +2106,7 @@ class SeatingTestCase(TestCase):
order=o, item=self.ticket, variation=None, price=Decimal("12"),
seat=self.seat_a1, subevent=se1
)
assert set(se1.free_seats) == set()
assert set(se1.free_seats()) == set()
assert not self.seat_a1.is_available()
@classscope(attr='organizer')
@@ -2123,7 +2123,7 @@ class SeatingTestCase(TestCase):
order=o, item=self.ticket, variation=None, price=Decimal("12"),
seat=self.seat_a1, subevent=se1
)
assert set(se1.free_seats) == {self.seat_a1}
assert set(se1.free_seats()) == {self.seat_a1}
assert self.seat_a1.is_available()
@classscope(attr='organizer')
@@ -2135,7 +2135,7 @@ class SeatingTestCase(TestCase):
event=self.event, cart_id='a', item=self.ticket, seat=self.seat_a1,
price=23, expires=now() + timedelta(minutes=10), subevent=se1
)
assert set(se1.free_seats) == set()
assert set(se1.free_seats()) == set()
assert not self.seat_a1.is_available()
@classscope(attr='organizer')
@@ -2147,7 +2147,25 @@ class SeatingTestCase(TestCase):
event=self.event, cart_id='a', item=self.ticket, seat=self.seat_a1,
price=23, expires=now() - timedelta(minutes=10), subevent=se1
)
assert set(se1.free_seats) == {self.seat_a1}
assert set(se1.free_seats()) == {self.seat_a1}
assert self.seat_a1.is_available()
@classscope(attr='organizer')
def test_voucher_active(self):
Voucher.objects.create(
event=self.event, code='a', item=self.ticket, seat=self.seat_a1,
valid_until=now() + timedelta(minutes=10)
)
assert set(self.event.free_seats()) == {self.seat_a2}
assert not self.seat_a1.is_available()
@classscope(attr='organizer')
def test_voucher_expired(self):
Voucher.objects.create(
event=self.event, code='a', item=self.ticket, seat=self.seat_a1,
valid_until=now() - timedelta(minutes=10)
)
assert set(self.event.free_seats()) == {self.seat_a2, self.seat_a1}
assert self.seat_a1.is_available()

View File

@@ -2918,6 +2918,30 @@ class CartSeatingTest(CartTestMixin, TestCase):
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
self.assertEqual(len(objs), 0)
def test_add_specific_voucher(self):
with scopes_disabled():
v = self.event.vouchers.create(item=self.ticket, seat=self.seat_a1)
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'seat_%d' % self.ticket.id: self.seat_a1,
'_voucher_code': v.code,
}, follow=True)
with scopes_disabled():
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
self.assertEqual(len(objs), 1)
self.assertEqual(objs[0].voucher, v)
self.assertEqual(objs[0].seat, self.seat_a1)
def test_add_specific_voucher_wrong_seat(self):
with scopes_disabled():
v = self.event.vouchers.create(item=self.ticket, seat=self.seat_a1)
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'seat_%d' % self.ticket.id: self.seat_a2,
'_voucher_code': v.code,
}, follow=True)
with scopes_disabled():
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
self.assertEqual(len(objs), 0)
def test_add_seat_unknown(self):
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
'seat_%d' % self.ticket.id: 'asdasdasd',