mirror of
https://github.com/pretix/pretix.git
synced 2026-08-19 12:16:26 +00:00
Add support for reserved seating (#1228)
* Initial work on seating * Add seat guids * Add product_list_top * CartAdd: Ignore item when a seat is passed * Cart display * product_list_top → render_seating_plan * Render seating plan in voucher redemption * Fix failing tests * Add tests for extending cart positions with seats * Add subevent_forms to docs * Update schema, migrations * Dealing with expired orders * steps to order change * Change order positions * Allow to add seats * tests for ocm * Fix things after rebase * Seating plans API * Add more tests for cart behaviour * Widget support * Adjust widget tests * Re-enable CSP * Update schema * Api: position.seat * Add guid to word list * API: (sub)event.seating_plan * Vali fixes * Fix api * Fix reference in test * Fix test for real
This commit is contained in:
+107
-1
@@ -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 Question
|
||||
from pretix.base.models import Question, SeatingPlan
|
||||
from pretix.base.models.orders import CartPosition
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ TEST_CARTPOSITION_RES = {
|
||||
'datetime': '2018-06-11T10:00:00Z',
|
||||
'expires': '2018-06-11T10:00:00Z',
|
||||
'includes_tax': True,
|
||||
'seat': None,
|
||||
'answers': []
|
||||
}
|
||||
|
||||
@@ -621,3 +622,108 @@ def test_cartpos_create_quota_validation(token_client, organizer, event, item, q
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['There is not enough quota available on quota "Budget Quota" to perform the operation.']
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seat(event, organizer, item):
|
||||
SeatingPlan.objects.create(
|
||||
name="Plan", organizer=organizer, layout="{}"
|
||||
)
|
||||
event.seat_category_mappings.create(
|
||||
layout_category='Stalls', product=item
|
||||
)
|
||||
return event.seats.create(name="A1", product=item, seat_guid="A1")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create_with_seat(token_client, organizer, event, item, quota, seat, question):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
res['seat'] = seat.seat_guid
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
p = CartPosition.objects.get(pk=resp.data['id'])
|
||||
assert p.seat == seat
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create_with_blocked_seat(token_client, organizer, event, item, quota, seat, question):
|
||||
seat.blocked = True
|
||||
seat.save()
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
res['seat'] = seat.seat_guid
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['The selected seat "A1" is not available.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create_with_used_seat(token_client, organizer, event, item, quota, seat, question):
|
||||
CartPosition.objects.create(
|
||||
event=event, cart_id='aaa', item=item,
|
||||
price=21.5, expires=now() + datetime.timedelta(minutes=10), seat=seat
|
||||
)
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
res['seat'] = seat.seat_guid
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['The selected seat "A1" is not available.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create_with_unknown_seat(token_client, organizer, event, item, quota, seat, question):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
res['seat'] = seat.seat_guid + '_'
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['The specified seat does not exist.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create_require_seat(token_client, organizer, event, item, quota, seat, question):
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['The specified product requires to choose a seat.']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_cartpos_create_unseated(token_client, organizer, event, item, quota, seat, question):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
quota.items.add(item2)
|
||||
res = copy.deepcopy(CARTPOS_CREATE_PAYLOAD)
|
||||
res['item'] = item2.pk
|
||||
res['seat'] = seat.seat_guid
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/cartpositions/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == ['The specified product does not allow to choose a seat.']
|
||||
|
||||
@@ -8,7 +8,9 @@ from django_countries.fields import Country
|
||||
from django_scopes import scopes_disabled
|
||||
from pytz import UTC
|
||||
|
||||
from pretix.base.models import Event, InvoiceAddress, Order, OrderPosition
|
||||
from pretix.base.models import (
|
||||
Event, InvoiceAddress, Order, OrderPosition, SeatingPlan,
|
||||
)
|
||||
from pretix.base.models.orders import OrderFee
|
||||
|
||||
|
||||
@@ -69,6 +71,8 @@ TEST_EVENT_RES = {
|
||||
"location": None,
|
||||
"slug": "dummy",
|
||||
"has_subevents": False,
|
||||
"seating_plan": None,
|
||||
"seat_category_mapping": {},
|
||||
"meta_data": {"type": "Conference"},
|
||||
'plugins': [
|
||||
'pretix.plugins.banktransfer',
|
||||
@@ -618,3 +622,303 @@ def test_event_with_order_position_not_delete(token_client, organizer, event, it
|
||||
'set \'live\' to false to hide the event and take the shop offline instead."}'
|
||||
with scopes_disabled():
|
||||
assert organizer.events.filter(pk=event.id).exists()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seatingplan(event, organizer, item):
|
||||
return SeatingPlan.objects.create(
|
||||
name="Plan", organizer=organizer, layout="""{
|
||||
"name": "Grosser Saal",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Stalls",
|
||||
"color": "red"
|
||||
}
|
||||
],
|
||||
"zones": [
|
||||
{
|
||||
"name": "Main Area",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"row_number": "0",
|
||||
"seats": [
|
||||
{
|
||||
"seat_guid": "0-0",
|
||||
"seat_number": "0-0",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"category": "Stalls"
|
||||
},
|
||||
{
|
||||
"seat_guid": "0-1",
|
||||
"seat_number": "0-1",
|
||||
"position": {
|
||||
"x": 33,
|
||||
"y": 0
|
||||
},
|
||||
"category": "Stalls"
|
||||
},
|
||||
{
|
||||
"seat_guid": "0-2",
|
||||
"seat_number": "0-2",
|
||||
"position": {
|
||||
"x": 66,
|
||||
"y": 0
|
||||
},
|
||||
"category": "Stalls"
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
}
|
||||
}"""
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_event_update_seating(token_client, organizer, event, item, seatingplan):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
event.refresh_from_db()
|
||||
assert event.seating_plan == seatingplan
|
||||
with scopes_disabled():
|
||||
assert event.seats.count() == 3
|
||||
assert event.seats.filter(product=item).count() == 3
|
||||
m = event.seat_category_mappings.get()
|
||||
assert m.layout_category == 'Stalls'
|
||||
assert m.product == item
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_event_update_seating_invalid_product(token_client, organizer, event, item, seatingplan):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk + 2
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"seat_category_mapping":["Item \'%d\' does not exist."]}' % (item.pk + 2)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_event_update_seating_change_mapping(token_client, organizer, event, item, seatingplan):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
event.refresh_from_db()
|
||||
assert event.seating_plan == seatingplan
|
||||
with scopes_disabled():
|
||||
assert event.seats.count() == 3
|
||||
assert event.seats.filter(product=item).count() == 3
|
||||
m = event.seat_category_mappings.get()
|
||||
assert m.layout_category == 'Stalls'
|
||||
assert m.product == item
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"seat_category_mapping": {
|
||||
"VIP": item.pk,
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
event.refresh_from_db()
|
||||
assert event.seating_plan == seatingplan
|
||||
with scopes_disabled():
|
||||
assert event.seats.count() == 3
|
||||
m = event.seat_category_mappings.get()
|
||||
assert event.seats.filter(product=None).count() == 3
|
||||
assert m.layout_category == 'VIP'
|
||||
assert m.product == item
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_remove_seating(token_client, organizer, event, item, seatingplan):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
event.refresh_from_db()
|
||||
assert event.seating_plan == seatingplan
|
||||
with scopes_disabled():
|
||||
assert event.seats.count() == 3
|
||||
assert event.seat_category_mappings.count() == 1
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"seating_plan": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
event.refresh_from_db()
|
||||
assert event.seating_plan is None
|
||||
with scopes_disabled():
|
||||
assert event.seats.count() == 0
|
||||
assert event.seat_category_mappings.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_remove_seating_forbidden(token_client, organizer, event, item, seatingplan, order_position):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
event.refresh_from_db()
|
||||
assert event.seating_plan == seatingplan
|
||||
with scopes_disabled():
|
||||
assert event.seats.count() == 3
|
||||
assert event.seat_category_mappings.count() == 1
|
||||
|
||||
order_position.seat = event.seats.first()
|
||||
order_position.save()
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"seating_plan": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"seating_plan":["You can not change the plan since seat \\"0-0\\" is not ' \
|
||||
'present in the new plan and is already sold."]}'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_no_seating_for_series(token_client, organizer, event, item, seatingplan, order_position):
|
||||
event.has_subevents = True
|
||||
event.save()
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"non_field_errors":["Event series should not directly be assigned a seating plan."]}'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_event_create_with_seating(token_client, organizer, event, meta_prop, seatingplan):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/'.format(organizer.slug),
|
||||
{
|
||||
"name": {
|
||||
"de": "Demo Konference 2020 Test",
|
||||
"en": "Demo Conference 2020 Test"
|
||||
},
|
||||
"live": False,
|
||||
"currency": "EUR",
|
||||
"date_from": "2017-12-27T10:00:00Z",
|
||||
"date_to": "2017-12-28T10:00:00Z",
|
||||
"date_admission": None,
|
||||
"is_public": False,
|
||||
"presale_start": None,
|
||||
"presale_end": None,
|
||||
"location": None,
|
||||
"slug": "2030",
|
||||
"seating_plan": seatingplan.pk,
|
||||
"meta_data": {
|
||||
meta_prop.name: "Conference"
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
event = Event.objects.get(slug=resp.data['slug'])
|
||||
assert event.seating_plan == seatingplan
|
||||
assert event.seats.count() == 3
|
||||
assert event.seat_category_mappings.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_event_create_with_seating_maps(token_client, organizer, event, meta_prop, seatingplan):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/'.format(organizer.slug),
|
||||
{
|
||||
"name": {
|
||||
"de": "Demo Konference 2020 Test",
|
||||
"en": "Demo Conference 2020 Test"
|
||||
},
|
||||
"live": False,
|
||||
"currency": "EUR",
|
||||
"date_from": "2017-12-27T10:00:00Z",
|
||||
"date_to": "2017-12-28T10:00:00Z",
|
||||
"date_admission": None,
|
||||
"is_public": False,
|
||||
"presale_start": None,
|
||||
"presale_end": None,
|
||||
"location": None,
|
||||
"slug": "2030",
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Foo": 1,
|
||||
},
|
||||
"meta_data": {
|
||||
meta_prop.name: "Conference"
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"seat_category_mapping":["You cannot specify seat category mappings on event creation."]}'
|
||||
|
||||
@@ -13,7 +13,9 @@ from pytz import UTC
|
||||
from stripe.error import APIConnectionError
|
||||
from tests.plugins.stripe.test_provider import MockedCharge
|
||||
|
||||
from pretix.base.models import InvoiceAddress, Order, OrderPosition, Question
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, Order, OrderPosition, Question, SeatingPlan,
|
||||
)
|
||||
from pretix.base.models.orders import (
|
||||
CartPosition, OrderFee, OrderPayment, OrderRefund,
|
||||
)
|
||||
@@ -139,6 +141,7 @@ TEST_ORDERPOSITION_RES = {
|
||||
"pseudonymization_id": "ABCDEFGHKL",
|
||||
"checkins": [],
|
||||
"downloads": [],
|
||||
"seat": None,
|
||||
"answers": [
|
||||
{
|
||||
"question": 1,
|
||||
@@ -2519,6 +2522,204 @@ def test_order_create_paid_generate_invoice(token_client, organizer, event, item
|
||||
assert p.payment_date.minute == 20
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seat(event, organizer, item):
|
||||
SeatingPlan.objects.create(
|
||||
name="Plan", organizer=organizer, layout="{}"
|
||||
)
|
||||
event.seat_category_mappings.create(
|
||||
layout_category='Stalls', product=item
|
||||
)
|
||||
return event.seats.create(name="A1", product=item, seat_guid="A1")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_with_seat(token_client, organizer, event, item, quota, seat, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['seat'] = seat.seat_guid
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
p = o.positions.first()
|
||||
assert p.seat == seat
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_with_blocked_seat(token_client, organizer, event, item, quota, seat, question):
|
||||
seat.blocked = True
|
||||
seat.save()
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['seat'] = seat.seat_guid
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'positions': [
|
||||
{'seat': ['The selected seat "A1" is not available.']},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_with_used_seat(token_client, organizer, event, item, quota, seat, question):
|
||||
CartPosition.objects.create(
|
||||
event=event, cart_id='aaa', item=item,
|
||||
price=21.5, expires=now() + datetime.timedelta(minutes=10), seat=seat
|
||||
)
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['seat'] = seat.seat_guid
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'positions': [
|
||||
{'seat': ['The selected seat "A1" is not available.']},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_with_unknown_seat(token_client, organizer, event, item, quota, seat, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['seat'] = seat.seat_guid + '_'
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'positions': [
|
||||
{'seat': ['The specified seat does not exist.']},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_require_seat(token_client, organizer, event, item, quota, seat, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'positions': [
|
||||
{'seat': ['The specified product requires to choose a seat.']},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_unseated(token_client, organizer, event, item, quota, seat, question):
|
||||
with scopes_disabled():
|
||||
item2 = event.items.create(name="Budget Ticket", default_price=23)
|
||||
quota.items.add(item2)
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item2.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['positions'][0]['seat'] = seat.seat_guid
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'positions': [
|
||||
{'seat': ['The specified product does not allow to choose a seat.']},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_with_duplicate_seat(token_client, organizer, event, item, quota, seat, question):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'] = [
|
||||
{
|
||||
"positionid": 1,
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": None,
|
||||
"answers": [],
|
||||
"subevent": None,
|
||||
"seat": seat.seat_guid
|
||||
},
|
||||
{
|
||||
"positionid": 2,
|
||||
"item": item.pk,
|
||||
"variation": None,
|
||||
"price": "23.00",
|
||||
"attendee_name_parts": {"full_name": "Peter"},
|
||||
"attendee_email": None,
|
||||
"addon_to": 1,
|
||||
"answers": [],
|
||||
"subevent": None,
|
||||
"seat": seat.seat_guid
|
||||
}
|
||||
]
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'positions': [
|
||||
{},
|
||||
{'seat': ['The selected seat "A1" is not available.']},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_with_seat_consumed_from_cart(token_client, organizer, event, item, quota, seat, question):
|
||||
CartPosition.objects.create(
|
||||
event=event, cart_id='aaa', item=item,
|
||||
price=21.5, expires=now() + datetime.timedelta(minutes=10), seat=seat
|
||||
)
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['seat'] = seat.seat_guid
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['consume_carts'] = ['aaa']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
p = o.positions.first()
|
||||
assert p.seat == seat
|
||||
|
||||
|
||||
REFUND_CREATE_PAYLOAD = {
|
||||
"state": "created",
|
||||
"provider": "manual",
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import copy
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.base.models import SeatingPlan
|
||||
|
||||
SAMPLE_PLAN = """{
|
||||
"name": "Sample plan",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Stalls",
|
||||
"color": "#33ffff"
|
||||
}
|
||||
],
|
||||
"zones": [
|
||||
{
|
||||
"name": "Main Area",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"row_number_position": "left",
|
||||
"rows": [
|
||||
{
|
||||
"row_number": "0",
|
||||
"seats": [
|
||||
{
|
||||
"seat_guid": "0-0",
|
||||
"seat_number": "0-0",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"category": "Stalls"
|
||||
},
|
||||
{
|
||||
"seat_guid": "0-1",
|
||||
"seat_number": "0-1",
|
||||
"position": {
|
||||
"x": 30,
|
||||
"y": 0
|
||||
},
|
||||
"category": "Stalls"
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"x": 40,
|
||||
"y": 25
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 500
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seatingplan(organizer, event):
|
||||
wh = organizer.seating_plans.create(
|
||||
name="Plan",
|
||||
layout=SAMPLE_PLAN
|
||||
)
|
||||
return wh
|
||||
|
||||
|
||||
TEST_PLAN_RES = {
|
||||
"id": 1,
|
||||
"name": "Plan",
|
||||
"layout": json.loads(SAMPLE_PLAN)
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_plan_list(token_client, organizer, event, seatingplan):
|
||||
res = dict(TEST_PLAN_RES)
|
||||
res["id"] = seatingplan.pk
|
||||
|
||||
resp = token_client.get('/api/v1/organizers/{}/seatingplans/'.format(organizer.slug))
|
||||
assert resp.status_code == 200
|
||||
assert [res] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_plan_detail(token_client, organizer, event, seatingplan):
|
||||
res = dict(TEST_PLAN_RES)
|
||||
res["id"] = seatingplan.pk
|
||||
resp = token_client.get('/api/v1/organizers/{}/seatingplans/{}/'.format(organizer.slug, seatingplan.pk))
|
||||
assert resp.status_code == 200
|
||||
assert res == resp.data
|
||||
|
||||
|
||||
TEST_PLAN_CREATE_PAYLOAD = {
|
||||
"name": "Plan 2",
|
||||
"layout": json.loads(SAMPLE_PLAN)
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_plan_create(token_client, organizer, event):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/seatingplans/'.format(organizer.slug),
|
||||
TEST_PLAN_CREATE_PAYLOAD,
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
cl = SeatingPlan.objects.get(pk=resp.data['id'])
|
||||
assert json.loads(cl.layout) == TEST_PLAN_CREATE_PAYLOAD['layout']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_plan_create_invalid_layout(token_client, organizer, event):
|
||||
res = copy.copy(TEST_PLAN_CREATE_PAYLOAD)
|
||||
res['layout'] = {'foo': 'bar'}
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/seatingplans/'.format(organizer.slug),
|
||||
res,
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_plan_patch(token_client, organizer, event, seatingplan):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/seatingplans/{}/'.format(organizer.slug, seatingplan.pk),
|
||||
{
|
||||
'name': 'Foo'
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
seatingplan.refresh_from_db()
|
||||
assert seatingplan.name == "Foo"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_plan_delete(token_client, organizer, event, seatingplan):
|
||||
resp = token_client.delete(
|
||||
'/api/v1/organizers/{}/seatingplans/{}/'.format(organizer.slug, seatingplan.pk),
|
||||
)
|
||||
assert resp.status_code == 204
|
||||
with scopes_disabled():
|
||||
assert SeatingPlan.objects.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_plan_patch_used(token_client, organizer, event, seatingplan):
|
||||
event.seating_plan = seatingplan
|
||||
event.save()
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/seatingplans/{}/'.format(organizer.slug, seatingplan.pk),
|
||||
{
|
||||
'name': 'Foo'
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_plan_delete_used(token_client, organizer, event, seatingplan):
|
||||
event.seating_plan = seatingplan
|
||||
event.save()
|
||||
resp = token_client.delete(
|
||||
'/api/v1/organizers/{}/seatingplans/{}/'.format(organizer.slug, seatingplan.pk),
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
@@ -7,7 +7,9 @@ from django_countries.fields import Country
|
||||
from django_scopes import scopes_disabled
|
||||
from pytz import UTC
|
||||
|
||||
from pretix.base.models import InvoiceAddress, Order, OrderPosition
|
||||
from pretix.base.models import (
|
||||
InvoiceAddress, Order, OrderPosition, SeatingPlan, SubEvent,
|
||||
)
|
||||
from pretix.base.models.orders import OrderFee
|
||||
|
||||
|
||||
@@ -72,6 +74,8 @@ TEST_SUBEVENT_RES = {
|
||||
'name': {'en': 'Foobar'},
|
||||
'date_from': '2017-12-27T10:00:00Z',
|
||||
'presale_end': None,
|
||||
'seating_plan': None,
|
||||
"seat_category_mapping": {},
|
||||
'id': 1,
|
||||
'variation_price_overrides': [],
|
||||
'location': None,
|
||||
@@ -573,3 +577,256 @@ def test_subevent_with_order_position_not_delete(token_client, organizer, event,
|
||||
'orders. Please set \'active\' to false instead to hide it from users."}'
|
||||
with scopes_disabled():
|
||||
assert event.subevents.filter(pk=subevent.id).exists()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seatingplan(event, organizer, item):
|
||||
return SeatingPlan.objects.create(
|
||||
name="Plan", organizer=organizer, layout="""{
|
||||
"name": "Grosser Saal",
|
||||
"categories": [
|
||||
{
|
||||
"name": "Stalls",
|
||||
"color": "red"
|
||||
}
|
||||
],
|
||||
"zones": [
|
||||
{
|
||||
"name": "Main Area",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"row_number": "0",
|
||||
"seats": [
|
||||
{
|
||||
"seat_guid": "0-0",
|
||||
"seat_number": "0-0",
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"category": "Stalls"
|
||||
},
|
||||
{
|
||||
"seat_guid": "0-1",
|
||||
"seat_number": "0-1",
|
||||
"position": {
|
||||
"x": 33,
|
||||
"y": 0
|
||||
},
|
||||
"category": "Stalls"
|
||||
},
|
||||
{
|
||||
"seat_guid": "0-2",
|
||||
"seat_number": "0-2",
|
||||
"position": {
|
||||
"x": 66,
|
||||
"y": 0
|
||||
},
|
||||
"category": "Stalls"
|
||||
}
|
||||
],
|
||||
"position": {
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
}
|
||||
}"""
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_subevent_update_seating(token_client, organizer, event, item, subevent, seatingplan):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||
{
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
subevent.refresh_from_db()
|
||||
assert subevent.seating_plan == seatingplan
|
||||
with scopes_disabled():
|
||||
assert subevent.seats.count() == 3
|
||||
assert subevent.seats.filter(product=item).count() == 3
|
||||
m = subevent.seat_category_mappings.get()
|
||||
assert m.layout_category == 'Stalls'
|
||||
assert m.product == item
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_subevent_update_seating_invalid_product(token_client, organizer, event, item, seatingplan, subevent):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||
{
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk + 2
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"seat_category_mapping":["Item \'%d\' does not exist."]}' % (item.pk + 2)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_subevent_update_seating_change_mapping(token_client, organizer, event, item, seatingplan, subevent):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||
{
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
subevent.refresh_from_db()
|
||||
assert subevent.seating_plan == seatingplan
|
||||
with scopes_disabled():
|
||||
assert subevent.seats.count() == 3
|
||||
assert subevent.seats.filter(product=item).count() == 3
|
||||
m = subevent.seat_category_mappings.get()
|
||||
assert m.layout_category == 'Stalls'
|
||||
assert m.product == item
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||
{
|
||||
"seat_category_mapping": {
|
||||
"VIP": item.pk,
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
subevent.refresh_from_db()
|
||||
assert subevent.seating_plan == seatingplan
|
||||
with scopes_disabled():
|
||||
assert subevent.seats.count() == 3
|
||||
m = subevent.seat_category_mappings.get()
|
||||
assert subevent.seats.filter(product=None).count() == 3
|
||||
assert m.layout_category == 'VIP'
|
||||
assert m.product == item
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_remove_seating(token_client, organizer, event, item, seatingplan, subevent):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||
{
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
subevent.refresh_from_db()
|
||||
assert subevent.seating_plan == seatingplan
|
||||
with scopes_disabled():
|
||||
assert subevent.seats.count() == 3
|
||||
assert subevent.seat_category_mappings.count() == 1
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||
{
|
||||
"seating_plan": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
subevent.refresh_from_db()
|
||||
assert subevent.seating_plan is None
|
||||
with scopes_disabled():
|
||||
assert subevent.seats.count() == 0
|
||||
assert subevent.seat_category_mappings.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_remove_seating_forbidden(token_client, organizer, event, item, seatingplan, order_position, subevent):
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||
{
|
||||
"seating_plan": seatingplan.pk,
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk
|
||||
}
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
subevent.refresh_from_db()
|
||||
assert subevent.seating_plan == seatingplan
|
||||
with scopes_disabled():
|
||||
assert subevent.seats.count() == 3
|
||||
assert subevent.seat_category_mappings.count() == 1
|
||||
|
||||
order_position.seat = subevent.seats.first()
|
||||
order_position.save()
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/subevents/{}/'.format(organizer.slug, event.slug, subevent.pk),
|
||||
{
|
||||
"seating_plan": None
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.content.decode() == '{"seating_plan":["You can not change the plan since seat \\"0-0\\" is not ' \
|
||||
'present in the new plan and is already sold."]}'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_subevent_create_with_seating(token_client, organizer, event, subevent, item, seatingplan):
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/subevents/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"name": {
|
||||
"de": "Demo Subevent 2020 Test",
|
||||
"en": "Demo Subevent 2020 Test"
|
||||
},
|
||||
"active": False,
|
||||
"date_from": "2017-12-27T10:00:00Z",
|
||||
"date_to": "2017-12-28T10:00:00Z",
|
||||
"date_admission": None,
|
||||
"presale_start": None,
|
||||
"presale_end": None,
|
||||
"location": None,
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": item.pk,
|
||||
"price": "23.42"
|
||||
}
|
||||
],
|
||||
"variation_price_overrides": [],
|
||||
"seat_category_mapping": {
|
||||
"Stalls": item.pk
|
||||
},
|
||||
"meta_data": {},
|
||||
"seating_plan": seatingplan.pk,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
subevent = SubEvent.objects.get(pk=resp.data['id'])
|
||||
assert subevent.seats.count() == 3
|
||||
assert subevent.seat_category_mappings.count() == 1
|
||||
|
||||
@@ -18,7 +18,7 @@ from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedFile, CartPosition, Checkin, CheckinList, Event, Item, ItemCategory,
|
||||
ItemVariation, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund,
|
||||
Organizer, Question, Quota, User, Voucher, WaitingListEntry,
|
||||
Organizer, Question, Quota, SeatingPlan, User, Voucher, WaitingListEntry,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import (
|
||||
@@ -1971,6 +1971,157 @@ class CheckinListTestCase(TestCase):
|
||||
assert lists[3].percent == 25
|
||||
|
||||
|
||||
class SeatingTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.organizer = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||
with scope(organizer=self.organizer):
|
||||
self.event = Event.objects.create(
|
||||
organizer=self.organizer, name='Dummy', slug='dummy',
|
||||
date_from=now(), date_to=now() - timedelta(hours=1),
|
||||
)
|
||||
self.ticket = self.event.items.create(name="Ticket", default_price=12)
|
||||
self.plan = SeatingPlan.objects.create(
|
||||
name="Plan", organizer=self.organizer, layout="{}"
|
||||
)
|
||||
self.event.seat_category_mappings.create(
|
||||
layout_category='Stalls', product=self.ticket
|
||||
)
|
||||
self.seat_a1 = self.event.seats.create(name="A1", product=self.ticket, blocked=False)
|
||||
self.seat_a2 = self.event.seats.create(name="A2", product=self.ticket, blocked=False)
|
||||
|
||||
@classscope(attr='organizer')
|
||||
def test_free(self):
|
||||
assert set(self.event.free_seats) == {self.seat_a1, self.seat_a2}
|
||||
assert self.seat_a1.is_available()
|
||||
assert self.seat_a2.is_available()
|
||||
|
||||
@classscope(attr='organizer')
|
||||
def test_blocked(self):
|
||||
self.seat_a1.blocked = True
|
||||
self.seat_a1.save()
|
||||
assert set(self.event.free_seats) == {self.seat_a2}
|
||||
assert not self.seat_a1.is_available()
|
||||
assert self.seat_a2.is_available()
|
||||
|
||||
@classscope(attr='organizer')
|
||||
def test_order_pending(self):
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
|
||||
locale='en', status=Order.STATUS_PENDING, datetime=now(),
|
||||
expires=now() + timedelta(days=10),
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o, item=self.ticket, variation=None, price=Decimal("12"),
|
||||
seat=self.seat_a1
|
||||
)
|
||||
assert set(self.event.free_seats) == {self.seat_a2}
|
||||
assert not self.seat_a1.is_available()
|
||||
|
||||
@classscope(attr='organizer')
|
||||
def test_order_paid(self):
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
|
||||
locale='en', status=Order.STATUS_PAID, datetime=now(),
|
||||
expires=now() + timedelta(days=10),
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o, item=self.ticket, variation=None, price=Decimal("12"),
|
||||
seat=self.seat_a1
|
||||
)
|
||||
assert set(self.event.free_seats) == {self.seat_a2}
|
||||
assert not self.seat_a1.is_available()
|
||||
|
||||
@classscope(attr='organizer')
|
||||
def test_order_expired(self):
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
|
||||
locale='en', status=Order.STATUS_EXPIRED, datetime=now(),
|
||||
expires=now() + timedelta(days=10),
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
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 self.seat_a1.is_available()
|
||||
|
||||
@classscope(attr='organizer')
|
||||
def test_cart_active(self):
|
||||
CartPosition.objects.create(
|
||||
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 not self.seat_a1.is_available()
|
||||
|
||||
@classscope(attr='organizer')
|
||||
def test_cart_expired(self):
|
||||
CartPosition.objects.create(
|
||||
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 self.seat_a1.is_available()
|
||||
|
||||
@classscope(attr='organizer')
|
||||
def test_subevent_order_pending(self):
|
||||
se1 = self.event.subevents.create(date_from=now(), name="SE 1")
|
||||
self.seat_a1.subevent = se1
|
||||
self.seat_a1.save()
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
|
||||
locale='en', status=Order.STATUS_PAID, datetime=now(),
|
||||
expires=now() + timedelta(days=10),
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o, item=self.ticket, variation=None, price=Decimal("12"),
|
||||
seat=self.seat_a1, subevent=se1
|
||||
)
|
||||
assert set(se1.free_seats) == set()
|
||||
assert not self.seat_a1.is_available()
|
||||
|
||||
@classscope(attr='organizer')
|
||||
def test_subevent_order_canceled(self):
|
||||
se1 = self.event.subevents.create(date_from=now(), name="SE 1")
|
||||
self.seat_a1.subevent = se1
|
||||
self.seat_a1.save()
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=self.event, email='dummy@dummy.test', total=Decimal("30"),
|
||||
locale='en', status=Order.STATUS_CANCELED, datetime=now(),
|
||||
expires=now() + timedelta(days=10),
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
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 self.seat_a1.is_available()
|
||||
|
||||
@classscope(attr='organizer')
|
||||
def test_subevent_cart_active(self):
|
||||
se1 = self.event.subevents.create(date_from=now(), name="SE 1")
|
||||
self.seat_a1.subevent = se1
|
||||
self.seat_a1.save()
|
||||
CartPosition.objects.create(
|
||||
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 not self.seat_a1.is_available()
|
||||
|
||||
@classscope(attr='organizer')
|
||||
def test_subevent_cart_expired(self):
|
||||
se1 = self.event.subevents.create(date_from=now(), name="SE 1")
|
||||
self.seat_a1.subevent = se1
|
||||
self.seat_a1.save()
|
||||
CartPosition.objects.create(
|
||||
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 self.seat_a1.is_available()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("qtype,answer,expected", [
|
||||
(Question.TYPE_STRING, "a", "a"),
|
||||
|
||||
@@ -12,6 +12,7 @@ from django_scopes import scope
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, InvoiceAddress, Item, Order, OrderPosition, Organizer,
|
||||
SeatingPlan,
|
||||
)
|
||||
from pretix.base.models.items import SubEventItem
|
||||
from pretix.base.models.orders import OrderFee, OrderPayment, OrderRefund
|
||||
@@ -635,6 +636,19 @@ class OrderChangeManagerTests(TestCase):
|
||||
self.quota.items.add(self.ticket2)
|
||||
self.quota.items.add(self.shirt)
|
||||
|
||||
self.stalls = Item.objects.create(event=self.event, name='Stalls', tax_rule=self.tr7,
|
||||
default_price=Decimal('23.00'), admission=True)
|
||||
self.plan = SeatingPlan.objects.create(
|
||||
name="Plan", organizer=self.o, layout="{}"
|
||||
)
|
||||
self.event.seat_category_mappings.create(
|
||||
layout_category='Stalls', product=self.stalls
|
||||
)
|
||||
self.quota.items.add(self.stalls)
|
||||
self.seat_a1 = self.event.seats.create(name="A1", product=self.stalls, seat_guid="A1")
|
||||
self.seat_a2 = self.event.seats.create(name="A2", product=self.stalls, seat_guid="A2")
|
||||
self.seat_a3 = self.event.seats.create(name="A3", product=self.stalls, seat_guid="A3")
|
||||
|
||||
def _enable_reverse_charge(self):
|
||||
self.tr7.eu_reverse_charge = True
|
||||
self.tr7.home_country = Country('DE')
|
||||
@@ -1631,3 +1645,206 @@ class OrderChangeManagerTests(TestCase):
|
||||
assert self.order.status == Order.STATUS_PENDING
|
||||
assert o2.total == Decimal('0.00')
|
||||
assert o2.status == Order.STATUS_PAID
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_seat_circular(self):
|
||||
self.op1.seat = self.seat_a1
|
||||
self.op1.save()
|
||||
self.op2.seat = self.seat_a2
|
||||
self.op2.save()
|
||||
self.ocm.change_seat(self.op1, self.seat_a2)
|
||||
self.ocm.change_seat(self.op2, self.seat_a1)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
self.op2.refresh_from_db()
|
||||
assert self.op1.seat == self.seat_a2
|
||||
assert self.op2.seat == self.seat_a1
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_seat_duplicate(self):
|
||||
self.op1.seat = self.seat_a1
|
||||
self.op1.save()
|
||||
self.op2.seat = self.seat_a2
|
||||
self.op2.save()
|
||||
self.ocm.change_seat(self.op1, self.seat_a2)
|
||||
with self.assertRaises(OrderError):
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.seat == self.seat_a1
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_seat_and_cancel(self):
|
||||
self.op1.seat = self.seat_a1
|
||||
self.op1.save()
|
||||
self.op2.seat = self.seat_a2
|
||||
self.op2.save()
|
||||
self.ocm.change_seat(self.op1, self.seat_a2)
|
||||
self.ocm.cancel(self.op2)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.seat == self.seat_a2
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_seat(self):
|
||||
self.op1.seat = self.seat_a1
|
||||
self.op1.save()
|
||||
self.ocm.change_seat(self.op1, self.seat_a2)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.seat == self.seat_a2
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_add_seat(self):
|
||||
# does this make sense or do we block it based on the item? this is currently not reachable through the UI
|
||||
self.ocm.change_seat(self.op1, self.seat_a1)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.seat == self.seat_a1
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_remove_seat(self):
|
||||
# does this make sense or do we block it based on the item? this is currently not reachable through the UI
|
||||
self.op1.seat = self.seat_a1
|
||||
self.op1.save()
|
||||
self.ocm.change_seat(self.op1, None)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.seat is None
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_add_with_seat(self):
|
||||
self.ocm.add_position(self.stalls, None, price=Decimal('13.00'), seat=self.seat_a3)
|
||||
self.ocm.commit()
|
||||
op3 = self.order.positions.last()
|
||||
assert op3.item == self.stalls
|
||||
assert op3.seat == self.seat_a3
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_add_with_taken_seat(self):
|
||||
self.op1.seat = self.seat_a1
|
||||
self.op1.save()
|
||||
self.ocm.add_position(self.stalls, None, price=Decimal('13.00'), seat=self.seat_a1)
|
||||
with self.assertRaises(OrderError):
|
||||
self.ocm.commit()
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_add_with_seat_required(self):
|
||||
with self.assertRaises(OrderError):
|
||||
self.ocm.add_position(self.stalls, None, price=Decimal('13.00'))
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_add_with_seat_forbidden(self):
|
||||
with self.assertRaises(OrderError):
|
||||
self.ocm.add_position(self.ticket, None, price=Decimal('13.00'), seat=self.seat_a1)
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_add_with_seat_blocked(self):
|
||||
self.seat_a1.blocked = True
|
||||
self.seat_a1.save()
|
||||
self.ocm.add_position(self.stalls, None, price=Decimal('13.00'), seat=self.seat_a1)
|
||||
with self.assertRaises(OrderError):
|
||||
self.ocm.commit()
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_seat_to_blocked(self):
|
||||
self.seat_a2.blocked = True
|
||||
self.seat_a2.save()
|
||||
self.op1.seat = self.seat_a1
|
||||
self.op1.save()
|
||||
self.ocm.change_seat(self.op1, self.seat_a2)
|
||||
with self.assertRaises(OrderError):
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.seat == self.seat_a1
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_seat_require_subevent_change(self):
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now())
|
||||
se2 = self.event.subevents.create(name="Bar", date_from=now())
|
||||
self.op1.subevent = se1
|
||||
self.op1.seat = self.seat_a1
|
||||
self.op1.save()
|
||||
self.quota.subevent = se2
|
||||
self.quota.save()
|
||||
self.seat_a1.subevent = se1
|
||||
self.seat_a1.save()
|
||||
self.seat_a2.subevent = se2
|
||||
self.seat_a2.save()
|
||||
self.ocm.change_seat(self.op1, self.seat_a2)
|
||||
with self.assertRaises(OrderError):
|
||||
self.ocm.commit()
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_subevent_require_seat_change(self):
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now())
|
||||
se2 = self.event.subevents.create(name="Bar", date_from=now())
|
||||
self.op1.subevent = se1
|
||||
self.op1.seat = self.seat_a1
|
||||
self.op1.save()
|
||||
self.quota.subevent = se2
|
||||
self.quota.save()
|
||||
self.seat_a1.subevent = se1
|
||||
self.seat_a1.save()
|
||||
self.seat_a2.subevent = se2
|
||||
self.seat_a2.save()
|
||||
self.ocm.change_subevent(self.op1, se2)
|
||||
with self.assertRaises(OrderError):
|
||||
self.ocm.commit()
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_subevent_and_seat(self):
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now())
|
||||
se2 = self.event.subevents.create(name="Bar", date_from=now())
|
||||
self.op1.subevent = se1
|
||||
self.op1.seat = self.seat_a1
|
||||
self.op1.save()
|
||||
self.quota.subevent = se2
|
||||
self.quota.save()
|
||||
self.seat_a1.subevent = se1
|
||||
self.seat_a1.save()
|
||||
self.seat_a2.subevent = se2
|
||||
self.seat_a2.save()
|
||||
self.ocm.change_subevent(self.op1, se2)
|
||||
self.ocm.change_seat(self.op1, self.seat_a2)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.seat == self.seat_a2
|
||||
assert self.op1.subevent == se2
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_change_seat_inside_subevent(self):
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now())
|
||||
self.op1.subevent = se1
|
||||
self.op1.seat = self.seat_a1
|
||||
self.op1.save()
|
||||
self.seat_a1.subevent = se1
|
||||
self.seat_a1.save()
|
||||
self.seat_a2.subevent = se1
|
||||
self.seat_a2.save()
|
||||
self.ocm.change_seat(self.op1, self.seat_a2)
|
||||
self.ocm.commit()
|
||||
self.op1.refresh_from_db()
|
||||
assert self.op1.seat == self.seat_a2
|
||||
assert self.op1.subevent == se1
|
||||
|
||||
@classscope(attr='o')
|
||||
def test_add_with_seat_and_subevent_mismatch(self):
|
||||
self.event.has_subevents = True
|
||||
self.event.save()
|
||||
se1 = self.event.subevents.create(name="Foo", date_from=now())
|
||||
se2 = self.event.subevents.create(name="Bar", date_from=now())
|
||||
self.quota.subevent = se2
|
||||
self.quota.save()
|
||||
self.seat_a1.subevent = se1
|
||||
self.seat_a1.save()
|
||||
self.ocm.change_subevent(self.op1, se2)
|
||||
with self.assertRaises(OrderError):
|
||||
self.ocm.add_position(self.ticket, None, price=Decimal('13.00'), subevent=se2, seat=self.seat_a1)
|
||||
|
||||
@@ -713,6 +713,103 @@ def test_order_extend_expired_quota_empty_ignore(client, env):
|
||||
assert o.status == Order.STATUS_PENDING
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_extend_expired_seat_free(client, env):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
o.expires = now() - timedelta(days=5)
|
||||
o.status = Order.STATUS_EXPIRED
|
||||
o.save()
|
||||
generate_cancellation(generate_invoice(o))
|
||||
seat_a1 = env[0].seats.create(name="A1", product=env[3], seat_guid="A1")
|
||||
p = o.positions.first()
|
||||
p.seat = seat_a1
|
||||
p.save()
|
||||
q = Quota.objects.create(event=env[0], size=3)
|
||||
q.items.add(env[3])
|
||||
newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
assert o.invoices.count() == 2
|
||||
response = client.post('/control/event/dummy/dummy/orders/FOO/extend', {
|
||||
'expires': newdate
|
||||
}, follow=True)
|
||||
assert b'alert-success' in response.content
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == newdate[:10] + " 23:59:59"
|
||||
assert o.status == Order.STATUS_PENDING
|
||||
assert o.invoices.count() == 3
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_extend_expired_seat_blocked(client, env):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
o.expires = now() - timedelta(days=5)
|
||||
o.status = Order.STATUS_EXPIRED
|
||||
olddate = o.expires
|
||||
o.save()
|
||||
seat_a1 = env[0].seats.create(name="A1", product=env[3], seat_guid="A1", blocked=True)
|
||||
p = o.positions.first()
|
||||
p.seat = seat_a1
|
||||
p.save()
|
||||
|
||||
q = Quota.objects.create(event=env[0], size=100)
|
||||
q.items.add(env[3])
|
||||
newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
response = client.post('/control/event/dummy/dummy/orders/FOO/extend', {
|
||||
'expires': newdate
|
||||
}, follow=True)
|
||||
assert b'alert-danger' in response.content
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == olddate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
assert o.status == Order.STATUS_EXPIRED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_extend_expired_seat_taken(client, env):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
o.expires = now() - timedelta(days=5)
|
||||
o.status = Order.STATUS_EXPIRED
|
||||
olddate = o.expires
|
||||
o.save()
|
||||
seat_a1 = env[0].seats.create(name="A1", product=env[3], seat_guid="A1")
|
||||
p = o.positions.first()
|
||||
p.seat = seat_a1
|
||||
p.save()
|
||||
|
||||
o = Order.objects.create(
|
||||
code='BAR', event=env[0], email='dummy@dummy.test',
|
||||
status=Order.STATUS_PENDING,
|
||||
datetime=now(), expires=now() + timedelta(days=10),
|
||||
total=14, locale='en'
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
item=env[3],
|
||||
variation=None,
|
||||
price=Decimal("14"),
|
||||
attendee_name_parts={'full_name': "Peter", "_scheme": "full"},
|
||||
seat=seat_a1
|
||||
)
|
||||
|
||||
q = Quota.objects.create(event=env[0], size=100)
|
||||
q.items.add(env[3])
|
||||
newdate = (now() + timedelta(days=20)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
response = client.post('/control/event/dummy/dummy/orders/FOO/extend', {
|
||||
'expires': newdate
|
||||
}, follow=True)
|
||||
assert b'alert-danger' in response.content
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == olddate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
assert o.status == Order.STATUS_EXPIRED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_extend_expired_quota_partial(client, env):
|
||||
with scopes_disabled():
|
||||
@@ -831,12 +928,57 @@ def test_order_mark_paid_forced(client, env):
|
||||
'amount': str(o.pending_sum),
|
||||
'force': 'on'
|
||||
}, follow=True)
|
||||
print(response.content.decode())
|
||||
assert 'alert-success' in response.content.decode()
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
assert o.status == Order.STATUS_PAID
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_mark_paid_expired_seat_taken(client, env):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
o.expires = now() - timedelta(days=5)
|
||||
o.status = Order.STATUS_EXPIRED
|
||||
olddate = o.expires
|
||||
o.save()
|
||||
seat_a1 = env[0].seats.create(name="A1", product=env[3], seat_guid="A1")
|
||||
p = o.positions.first()
|
||||
p.seat = seat_a1
|
||||
p.save()
|
||||
|
||||
o = Order.objects.create(
|
||||
code='BAR', event=env[0], email='dummy@dummy.test',
|
||||
status=Order.STATUS_PENDING,
|
||||
datetime=now(), expires=now() + timedelta(days=10),
|
||||
total=14, locale='en'
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
item=env[3],
|
||||
variation=None,
|
||||
price=Decimal("14"),
|
||||
attendee_name_parts={'full_name': "Peter", "_scheme": "full"},
|
||||
seat=seat_a1
|
||||
)
|
||||
|
||||
q = Quota.objects.create(event=env[0], size=100)
|
||||
q.items.add(env[3])
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
response = client.post('/control/event/dummy/dummy/orders/FOO/transition', {
|
||||
'status': 'p',
|
||||
'payment_date': now().date().isoformat(),
|
||||
'amount': str(o.pending_sum),
|
||||
'force': 'on'
|
||||
}, follow=True)
|
||||
assert b'alert-danger' in response.content
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(id=env[2].id)
|
||||
assert o.expires.strftime("%Y-%m-%d %H:%M:%S") == olddate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
assert o.status == Order.STATUS_EXPIRED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_go_lowercase(client, env):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
|
||||
@@ -12,7 +12,7 @@ from django_scopes import scopes_disabled
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, InvoiceAddress, Item, ItemCategory, ItemVariation,
|
||||
Organizer, Question, QuestionAnswer, Quota, Voucher,
|
||||
Organizer, Question, QuestionAnswer, Quota, SeatingPlan, Voucher,
|
||||
)
|
||||
from pretix.base.models.items import (
|
||||
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
|
||||
@@ -2826,3 +2826,159 @@ class CartBundleTest(CartTestMixin, TestCase):
|
||||
assert a.price == Decimal('1.40')
|
||||
assert a.tax_rate == Decimal('0.00')
|
||||
assert not a.includes_tax
|
||||
|
||||
|
||||
class CartSeatingTest(CartTestMixin, TestCase):
|
||||
|
||||
@scopes_disabled()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.plan = SeatingPlan.objects.create(
|
||||
name="Plan", organizer=self.orga, layout="{}"
|
||||
)
|
||||
self.event.seat_category_mappings.create(
|
||||
layout_category='Stalls', product=self.ticket
|
||||
)
|
||||
self.seat_a1 = self.event.seats.create(name="A1", product=self.ticket, seat_guid="A1")
|
||||
self.seat_a2 = self.event.seats.create(name="A2", product=self.ticket, seat_guid="A2")
|
||||
self.seat_a3 = self.event.seats.create(name="A3", product=self.ticket, seat_guid="A3")
|
||||
self.cm = CartManager(event=self.event, cart_id=self.session_key)
|
||||
|
||||
def test_add_with_seat_without_variation(self):
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'seat_%d' % self.ticket.id: self.seat_a1.seat_guid,
|
||||
}, 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].item, self.ticket)
|
||||
self.assertEqual(objs[0].seat, self.seat_a1)
|
||||
self.assertIsNone(objs[0].variation)
|
||||
self.assertEqual(objs[0].price, 23)
|
||||
|
||||
def test_add_with_seat_with_missing_variation(self):
|
||||
with scopes_disabled():
|
||||
v1 = self.ticket.variations.create(value='Regular', active=True)
|
||||
self.quota_tickets.variations.add(v1)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'seat_%d' % self.ticket.id: self.seat_a1.seat_guid,
|
||||
}, 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_blocked(self):
|
||||
self.seat_a1.blocked = True
|
||||
self.seat_a1.save()
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'seat_%d' % self.ticket.id: self.seat_a1.seat_guid,
|
||||
}, 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_unseated_product(self):
|
||||
with scopes_disabled():
|
||||
self.event.seat_category_mappings.all().delete()
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'seat_%d' % self.ticket.id: self.seat_a1.seat_guid,
|
||||
}, 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_wrong_product(self):
|
||||
with scopes_disabled():
|
||||
self.event.seat_category_mappings.all().update(product=self.shirt)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'seat_%d' % self.ticket.id: self.seat_a1.seat_guid,
|
||||
}, 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',
|
||||
}, 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_required(self):
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_%d' % self.ticket.id: '1',
|
||||
}, 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_with_seat_with_variation(self):
|
||||
with scopes_disabled():
|
||||
v1 = self.ticket.variations.create(value='Regular', active=True)
|
||||
self.quota_tickets.variations.add(v1)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'seat_%d_%d' % (self.ticket.id, v1.pk): self.seat_a1.seat_guid,
|
||||
}, 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].item, self.ticket)
|
||||
self.assertEqual(objs[0].seat, self.seat_a1)
|
||||
self.assertEqual(objs[0].variation, v1)
|
||||
self.assertEqual(objs[0].price, 23)
|
||||
|
||||
def test_add_with_seat_to_cart_twice(self):
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket, seat=self.seat_a1,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
with scopes_disabled():
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 1)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'seat_%d' % self.ticket.id: self.seat_a1.seat_guid,
|
||||
}, 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].seat, self.seat_a1)
|
||||
|
||||
def test_add_used_seat_to_cart(self):
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id='aaa', item=self.ticket, seat=self.seat_a1,
|
||||
price=23, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'seat_%d' % self.ticket.id: self.seat_a1.seat_guid,
|
||||
}, follow=True)
|
||||
with scopes_disabled():
|
||||
objs = list(CartPosition.objects.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 0)
|
||||
|
||||
@scopes_disabled()
|
||||
def test_extend_seat_still_available(self):
|
||||
with scopes_disabled():
|
||||
cp = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket, seat=self.seat_a1,
|
||||
price=21.5, expires=now() - timedelta(minutes=10)
|
||||
)
|
||||
self.cm.commit()
|
||||
cp.refresh_from_db()
|
||||
assert cp.seat == self.seat_a1
|
||||
|
||||
@scopes_disabled()
|
||||
def test_extend_seat_taken(self):
|
||||
with scopes_disabled():
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket, seat=self.seat_a1,
|
||||
price=21.5, expires=now() - timedelta(minutes=10)
|
||||
)
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id='secondcart', item=self.ticket, seat=self.seat_a1,
|
||||
price=21.5, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
with self.assertRaises(CartError):
|
||||
self.cm.commit()
|
||||
|
||||
assert not CartPosition.objects.filter(cart_id=self.session_key).exists()
|
||||
|
||||
@@ -17,7 +17,7 @@ from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, Invoice, InvoiceAddress, Item, ItemCategory, Order,
|
||||
OrderPayment, OrderPosition, Organizer, Question, QuestionAnswer, Quota,
|
||||
Voucher,
|
||||
SeatingPlan, Voucher,
|
||||
)
|
||||
from pretix.base.models.items import (
|
||||
ItemAddOn, ItemBundle, ItemVariation, SubEventItem,
|
||||
@@ -2590,3 +2590,86 @@ class CheckoutBundleTest(BaseCheckoutTestCase, TestCase):
|
||||
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
doc = BeautifulSoup(response.rendered_content, "lxml")
|
||||
self.assertEqual(len(doc.select(".thank-you")), 1)
|
||||
|
||||
|
||||
class CheckoutSeatingTest(BaseCheckoutTestCase, TestCase):
|
||||
@scopes_disabled()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.plan = SeatingPlan.objects.create(
|
||||
name="Plan", organizer=self.orga, layout="{}"
|
||||
)
|
||||
self.event.seat_category_mappings.create(
|
||||
layout_category='Stalls', product=self.ticket
|
||||
)
|
||||
self.seat_a1 = self.event.seats.create(name="A1", product=self.ticket, seat_guid="A1")
|
||||
self.seat_a2 = self.event.seats.create(name="A2", product=self.ticket, seat_guid="A2")
|
||||
self.seat_a3 = self.event.seats.create(name="A3", product=self.ticket, seat_guid="A3")
|
||||
self.cp1 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=21.5, expires=now() + timedelta(minutes=10), seat=self.seat_a1
|
||||
)
|
||||
|
||||
@scopes_disabled()
|
||||
def test_passes(self):
|
||||
oid = _perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
o = Order.objects.get(pk=oid)
|
||||
op = o.positions.first()
|
||||
assert op.item == self.ticket
|
||||
assert op.seat == self.seat_a1
|
||||
|
||||
@scopes_disabled()
|
||||
def test_seat_required(self):
|
||||
self.cp1.seat = None
|
||||
self.cp1.save()
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
assert not CartPosition.objects.filter(pk=self.cp1.pk).exists()
|
||||
|
||||
@scopes_disabled()
|
||||
def test_seat_not_allowed(self):
|
||||
self.cp1.item = self.workshop1
|
||||
self.cp1.save()
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
assert not CartPosition.objects.filter(pk=self.cp1.pk).exists()
|
||||
|
||||
@scopes_disabled()
|
||||
def test_seat_invalid_product(self):
|
||||
self.cp1.item = self.workshop1
|
||||
self.cp1.save()
|
||||
self.event.seat_category_mappings.create(
|
||||
layout_category='Foo', product=self.workshop1
|
||||
)
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
assert not CartPosition.objects.filter(pk=self.cp1.pk).exists()
|
||||
|
||||
@scopes_disabled()
|
||||
def test_seat_multiple_times_same_seat(self):
|
||||
cp2 = CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=21.5, expires=now() + timedelta(minutes=10), seat=self.seat_a1
|
||||
)
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk, cp2.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
assert not CartPosition.objects.filter(pk=self.cp1.pk).exists()
|
||||
assert not CartPosition.objects.filter(pk=cp2.pk).exists()
|
||||
|
||||
@scopes_disabled()
|
||||
def test_seat_blocked(self):
|
||||
self.seat_a1.blocked = True
|
||||
self.seat_a1.save()
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
assert not CartPosition.objects.filter(pk=self.cp1.pk).exists()
|
||||
|
||||
@scopes_disabled()
|
||||
def test_seat_taken(self):
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key + '_other', item=self.ticket,
|
||||
price=21.5, expires=now() + timedelta(minutes=10), seat=self.seat_a1
|
||||
)
|
||||
with self.assertRaises(OrderError):
|
||||
_perform_order(self.event, 'manual', [self.cp1.pk], 'admin@example.org', 'en', None, {}, 'web')
|
||||
assert not CartPosition.objects.filter(pk=self.cp1.pk).exists()
|
||||
|
||||
@@ -133,6 +133,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"vouchers_exist": False,
|
||||
"waiting_list_enabled": False,
|
||||
"error": None,
|
||||
"has_seating_plan": False,
|
||||
"items_by_category": [
|
||||
{
|
||||
"items": [
|
||||
@@ -214,6 +215,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"currency": "EUR",
|
||||
"show_variations_expanded": False,
|
||||
"display_net_prices": False,
|
||||
"has_seating_plan": False,
|
||||
"vouchers_exist": True,
|
||||
"waiting_list_enabled": False,
|
||||
"error": None,
|
||||
@@ -262,6 +264,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"show_variations_expanded": False,
|
||||
"display_net_prices": False,
|
||||
"vouchers_exist": True,
|
||||
"has_seating_plan": False,
|
||||
"waiting_list_enabled": False,
|
||||
"error": None,
|
||||
"items_by_category": [
|
||||
@@ -324,6 +327,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"currency": "EUR",
|
||||
"show_variations_expanded": False,
|
||||
"display_net_prices": False,
|
||||
"has_seating_plan": False,
|
||||
"vouchers_exist": True,
|
||||
"waiting_list_enabled": False,
|
||||
"error": "This voucher is expired.",
|
||||
|
||||
Reference in New Issue
Block a user