Fix #515 -- Add check-in lists (#693)

* Data model and migration

* Some backwards compatibility

* CRUD for checkin lists

* Show and perform checkins

* Correct numbers in table and dashboard widget

* event creation and cloning

* Allow to link specific exports and pass options per query

* Play with the CSV export

* PDF export

* Collapse exports by default

* Improve PDF exporter

* Addon stuff

* Subevent stuff, pretixdroid tests

* pretixdroid tests

* Add CRUD API

* Test compatibility

* Fix test

* DB-independent sorting behavior

* Add CRUD and coyp tests

* Re-enable pretixdroid plugin

* pretixdroid config

* Tests & fixes
This commit is contained in:
Raphael Michel
2017-12-04 18:12:23 +01:00
committed by GitHub
parent f1be7ed69d
commit 353dce789d
58 changed files with 2402 additions and 608 deletions

View File

@@ -1,4 +1,4 @@
from datetime import timedelta
from datetime import datetime, timedelta, timezone
from decimal import Decimal
import pytest
@@ -10,6 +10,8 @@ from pretix.base.models import (
)
from pretix.control.views.dashboards import checkin_widget
from ..base import SoupTest, extract_form_fields
@pytest.fixture
def dashboard_env():
@@ -27,6 +29,8 @@ def dashboard_env():
t.members.add(user)
t.limit_events.add(event)
cl = event.checkin_lists.create(name="Default", all_products=True)
event.settings.set('attendee_names_asked', True)
event.settings.set('locales', ['en', 'de'])
@@ -50,7 +54,7 @@ def dashboard_env():
price=Decimal("10")
)
return event, user, o, order_paid, item_ticket, item_mascot
return event, user, o, order_paid, item_ticket, item_mascot, cl
@pytest.mark.django_db
@@ -84,32 +88,11 @@ def test_dashboard_with_checkin(dashboard_env):
order=dashboard_env[3],
item=dashboard_env[4]
)
Checkin.objects.create(position=op)
Checkin.objects.create(position=op, list=dashboard_env[6])
c = checkin_widget(dashboard_env[0])
assert '1/2' in c[0]['content']
@pytest.mark.django_db
def test_dashboard_exclude_non_admission_item(dashboard_env):
dashboard_env[0].settings.ticket_download_nonadm = False
dashboard_env[0].save()
c = checkin_widget(dashboard_env[0])
assert '0/1' in c[0]['content']
@pytest.mark.django_db
def test_dashboard_exclude_non_admission_item_with_checkin(dashboard_env):
dashboard_env[0].settings.ticket_download_nonadm = False
dashboard_env[0].save()
op = OrderPosition.objects.get(
order=dashboard_env[3],
item=dashboard_env[4]
)
Checkin.objects.create(position=op)
c = checkin_widget(dashboard_env[0])
assert '1/1' in c[0]['content']
@pytest.fixture
def checkin_list_env():
# permission
@@ -128,9 +111,11 @@ def checkin_list_env():
event.settings.set('locales', ['en', 'de'])
team.limit_events.add(event)
cl = event.checkin_lists.create(name="Default", all_products=True)
# item
item_ticket = Item.objects.create(event=event, name="Ticket", default_price=23, admission=True)
item_mascot = Item.objects.create(event=event, name="Mascot", default_price=10, admission=False)
item_ticket = Item.objects.create(event=event, name="Ticket", default_price=23, admission=True, position=0)
item_mascot = Item.objects.create(event=event, name="Mascot", default_price=10, admission=False, position=1)
# order
order_pending = Order.objects.create(
@@ -196,32 +181,32 @@ def checkin_list_env():
)
# checkin
Checkin.objects.create(position=op_a1_ticket, datetime=now() + timedelta(minutes=1))
Checkin.objects.create(position=op_a3_ticket)
Checkin.objects.create(position=op_a1_ticket, datetime=now() + timedelta(minutes=1), list=cl)
Checkin.objects.create(position=op_a3_ticket, list=cl)
return event, user, orga, [item_ticket, item_mascot], [order_pending, order_a1, order_a2, order_a3], \
[op_pending_ticket, op_a1_ticket, op_a1_mascot, op_a2_ticket, op_a3_ticket]
[op_pending_ticket, op_a1_ticket, op_a1_mascot, op_a2_ticket, op_a3_ticket], cl
@pytest.mark.django_db
@pytest.mark.parametrize("order_key, expected", [
('', ['A1Ticket', 'A1Mascot', 'A2Ticket', 'A3Ticket']),
('-code', ['A3Ticket', 'A2Ticket', 'A1Ticket', 'A1Mascot']),
('code', ['A1Ticket', 'A1Mascot', 'A2Ticket', 'A3Ticket']),
('code', ['A1Mascot', 'A1Ticket', 'A2Ticket', 'A3Ticket']),
('-email', ['A3Ticket', 'A2Ticket', 'A1Ticket', 'A1Mascot']),
('email', ['A1Ticket', 'A1Mascot', 'A2Ticket', 'A3Ticket']),
('-status', ['A3Ticket', 'A1Ticket', 'A1Mascot', 'A2Ticket']),
('email', ['A1Mascot', 'A1Ticket', 'A2Ticket', 'A3Ticket']),
('-status', ['A3Ticket', 'A1Ticket', 'A2Ticket', 'A1Mascot']),
('status', ['A1Mascot', 'A2Ticket', 'A1Ticket', 'A3Ticket']),
('-timestamp', ['A1Ticket', 'A3Ticket', 'A1Mascot', 'A2Ticket']), # A1 checkin date > A3 checkin date
('-timestamp', ['A1Ticket', 'A3Ticket', 'A2Ticket', 'A1Mascot']), # A1 checkin date > A3 checkin date
('timestamp', ['A1Mascot', 'A2Ticket', 'A3Ticket', 'A1Ticket']),
('-name', ['A3Ticket', 'A2Ticket', 'A1Ticket', 'A1Mascot']),
('name', ['A1Mascot', 'A1Ticket', 'A2Ticket', 'A3Ticket']), # mascot doesn't include attendee name
('-item', ['A1Ticket', 'A2Ticket', 'A3Ticket', 'A1Mascot']),
('-item', ['A3Ticket', 'A2Ticket', 'A1Ticket', 'A1Mascot']),
('item', ['A1Mascot', 'A1Ticket', 'A2Ticket', 'A3Ticket']),
])
def test_checkins_list_ordering(client, checkin_list_env, order_key, expected):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/checkins/?ordering=' + order_key)
response = client.get('/control/event/dummy/dummy/checkinlists/{}/?ordering='.format(checkin_list_env[6].pk) + order_key)
qs = response.context['entries']
item_keys = [q.order.code + str(q.item.name) for q in qs]
assert item_keys == expected
@@ -240,7 +225,7 @@ def test_checkins_list_ordering(client, checkin_list_env, order_key, expected):
])
def test_checkins_list_filter(client, checkin_list_env, query, expected):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/checkins/?' + query)
response = client.get('/control/event/dummy/dummy/checkinlists/{}/?'.format(checkin_list_env[6].pk) + query)
qs = response.context['entries']
item_keys = [q.order.code + str(q.item.name) for q in qs]
print([str(item.name) + '-' + str(item.id) for item in Item.objects.all()])
@@ -251,7 +236,7 @@ def test_checkins_list_filter(client, checkin_list_env, query, expected):
def test_checkins_item_filter(client, checkin_list_env):
client.login(email='dummy@dummy.dummy', password='dummy')
for item in checkin_list_env[3]:
response = client.get('/control/event/dummy/dummy/checkins/?item=' + str(item.id))
response = client.get('/control/event/dummy/dummy/checkinlists/{}/?item={}'.format(checkin_list_env[6].pk, item.pk))
assert all(i.item.id == item.id for i in response.context['entries'])
@@ -263,7 +248,7 @@ def test_checkins_item_filter(client, checkin_list_env):
])
def test_checkins_list_mixed(client, checkin_list_env, query, expected):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/checkins/?' + query)
response = client.get('/control/event/dummy/dummy/checkinlists/{}/?{}'.format(checkin_list_env[6].pk, query))
qs = response.context['entries']
item_keys = [q.order.code + str(q.item.name) for q in qs]
assert item_keys == expected
@@ -273,7 +258,7 @@ def test_checkins_list_mixed(client, checkin_list_env, query, expected):
def test_manual_checkins(client, checkin_list_env):
client.login(email='dummy@dummy.dummy', password='dummy')
assert not checkin_list_env[5][3].checkins.exists()
client.post('/control/event/dummy/dummy/checkins/', {
client.post('/control/event/dummy/dummy/checkinlists/{}/'.format(checkin_list_env[6].pk), {
'checkin': [checkin_list_env[5][3].pk]
})
assert checkin_list_env[5][3].checkins.exists()
@@ -299,6 +284,7 @@ def checkin_list_with_addon_env():
event.settings.set('attendee_names_asked', True)
event.settings.set('locales', ['en', 'de'])
team.limit_events.add(event)
cl = event.checkin_lists.create(name="Default", all_products=True)
# item
cat_adm = ItemCategory.objects.create(event=event, name="Admission")
@@ -359,29 +345,69 @@ def checkin_list_with_addon_env():
)
# checkin
Checkin.objects.create(position=op_a1_ticket, datetime=now() + timedelta(minutes=1))
Checkin.objects.create(position=op_a1_ticket, datetime=now() + timedelta(minutes=1), list=cl)
return event, user, orga, [item_ticket, item_workshop], [order_pending, order_a1, order_a2], \
[op_pending_ticket, op_a1_ticket, op_a1_workshop, op_a2_ticket]
[op_pending_ticket, op_a1_ticket, op_a1_workshop, op_a2_ticket], cl
@pytest.mark.django_db
def test_checkins_attendee_name_from_addon_available(client, checkin_list_with_addon_env):
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/checkins/')
response = client.get('/control/event/dummy/dummy/checkinlists/{}/'.format(checkin_list_with_addon_env[6].pk))
qs = response.context['entries']
item_keys = [q.order.code + str(q.item.name) +
(str(q.addon_to.attendee_name) if q.addon_to is not None else str(q.attendee_name)) for q in qs]
assert item_keys == ['A1TicketA1', 'A1WorkshopA1', 'A2TicketA2'] # A1Workshop<name> comes from addon_to position
@pytest.mark.django_db
def test_checkins_with_noadm_option(client, checkin_list_with_addon_env):
checkin_list_with_addon_env[0].settings.ticket_download_nonadm = False
checkin_list_with_addon_env[0].save()
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/checkins/')
qs = response.context['entries']
item_keys = [q.order.code + str(q.item.name) +
(str(q.addon_to.attendee_name) if q.addon_to is not None else str(q.attendee_name)) for q in qs]
assert item_keys == ['A1TicketA1', 'A2TicketA2']
class CheckinListFormTest(SoupTest):
def setUp(self):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
self.orga1 = Organizer.objects.create(name='CCC', slug='ccc')
self.orga2 = Organizer.objects.create(name='MRM', slug='mrm')
self.event1 = Event.objects.create(
organizer=self.orga1, name='30C3', slug='30c3',
date_from=datetime(2013, 12, 26, tzinfo=timezone.utc),
)
t = Team.objects.create(organizer=self.orga1, can_change_event_settings=True, can_view_orders=True)
t.members.add(self.user)
t.limit_events.add(self.event1)
self.client.login(email='dummy@dummy.dummy', password='dummy')
self.item_ticket = Item.objects.create(event=self.event1, name="Ticket", default_price=23, admission=True)
def test_create(self):
doc = self.get_doc('/control/event/%s/%s/checkinlists/add' % (self.orga1.slug, self.event1.slug))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
form_data['name'] = 'All'
form_data['all_products'] = 'on'
doc = self.post_doc('/control/event/%s/%s/checkinlists/add' % (self.orga1.slug, self.event1.slug), form_data)
assert doc.select(".alert-success")
self.assertIn("All", doc.select("#page-wrapper table")[0].text)
assert self.event1.checkin_lists.get(
name='All', all_products=True
)
def test_update(self):
cl = self.event1.checkin_lists.create(name='All', all_products=True)
doc = self.get_doc('/control/event/%s/%s/checkinlists/%s/change' % (self.orga1.slug, self.event1.slug, cl.id))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
form_data['all_products'] = ''
form_data['limit_products'] = str(self.item_ticket.pk)
doc = self.post_doc('/control/event/%s/%s/checkinlists/%s/change' % (self.orga1.slug, self.event1.slug, cl.id),
form_data)
assert doc.select(".alert-success")
cl.refresh_from_db()
assert not cl.all_products
assert list(cl.limit_products.all()) == [self.item_ticket]
def test_delete(self):
cl = self.event1.checkin_lists.create(name='All', all_products=True)
doc = self.get_doc('/control/event/%s/%s/checkinlists/%s/delete' % (self.orga1.slug, self.event1.slug, cl.id))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
doc = self.post_doc('/control/event/%s/%s/checkinlists/%s/delete' % (self.orga1.slug, self.event1.slug, cl.id),
form_data)
assert doc.select(".alert-success")
self.assertNotIn("VAT", doc.select("#page-wrapper")[0].text)
assert not self.event1.checkin_lists.exists()

View File

@@ -613,6 +613,12 @@ class SubEventsTest(SoupTest):
'location_0': 'Hamburg',
'presale_start_0': '2017-06-20',
'presale_start_1': '10:00:00',
'checkinlist_set-TOTAL_FORMS': '1',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
'checkinlist_set-0-name': 'Default',
'checkinlist_set-0-all_products': 'on',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
@@ -638,6 +644,7 @@ class SubEventsTest(SoupTest):
assert list(q.items.all()) == [self.ticket]
sei = SubEventItem.objects.get(subevent=se, item=self.ticket)
assert sei.price == 12
assert se.checkinlist_set.count() == 1
def test_modify(self):
doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk)
@@ -659,8 +666,15 @@ class SubEventsTest(SoupTest):
'quotas-0-name': 'Q1',
'quotas-0-size': '50',
'quotas-0-itemvars': str(self.ticket.pk),
'checkinlist_set-TOTAL_FORMS': '1',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
'checkinlist_set-0-name': 'Default',
'checkinlist_set-0-all_products': 'on',
'item-%d-price' % self.ticket.pk: '12'
})
print(doc)
assert doc.select(".alert-success")
self.subevent1.refresh_from_db()
se = self.subevent1
@@ -678,6 +692,7 @@ class SubEventsTest(SoupTest):
assert list(q.items.all()) == [self.ticket]
sei = SubEventItem.objects.get(subevent=se, item=self.ticket)
assert sei.price == 12
assert se.checkinlist_set.count() == 1
def test_delete(self):
doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk)

View File

@@ -89,6 +89,10 @@ event_urls = [
"orders/ABC/checkvatid",
"orders/ABC/",
"orders/",
"checkinlists/",
"checkinlists/1/",
"checkinlists/1/change",
"checkinlists/1/delete",
"waitinglist/",
"waitinglist/auto_assign",
"invoice/1",
@@ -225,7 +229,11 @@ event_permission_urls = [
("can_change_vouchers", "vouchers/1234/delete", 404),
("can_view_orders", "waitinglist/", 200),
("can_change_orders", "waitinglist/auto_assign", 405),
("can_view_orders", "checkins/", 200),
("can_view_orders", "checkinlists/", 200),
("can_view_orders", "checkinlists/1/", 404),
("can_change_event_settings", "checkinlists/add", 200),
("can_change_event_settings", "checkinlists/1/change", 404),
("can_change_event_settings", "checkinlists/1/delete", 404),
]