diff --git a/src/pretix/base/models/checkin.py b/src/pretix/base/models/checkin.py index 4e4018dbc5..e9e8de1fcc 100644 --- a/src/pretix/base/models/checkin.py +++ b/src/pretix/base/models/checkin.py @@ -17,7 +17,18 @@ class CheckinList(LoggedModel): @staticmethod def annotate_with_numbers(qs, event): - from . import Order, OrderPosition + """ + Modifies a queryset of checkin lists by annotating it with the number of order positions and + checkins associated with it. + """ + # Import here to prevent circular import + from . import Order, OrderPosition, Item + + # This is the mother of all subqueries. Sorry. I try to explain it, at least? + # First, we prepare a subquery that for every check-in that belongs to a paid-order + # position and to the list in question. Then, we check that it also belongs to the + # correct subevent (just to be sure) and aggregate over lists (so, over everything, + # since we filtered by lists). cqs = Checkin.objects.filter( position__order__event=event, position__order__status=Order.STATUS_PAID, @@ -30,6 +41,11 @@ class CheckinList(LoggedModel): ).order_by().values('list').annotate( c=Count('*') ).values('c') + + # Now for the hard part: getting all order positions that contribute to this list. This + # requires us to use TWO subqueries. The first one, pqs_all, will only be used for check-in + # lists that contain all the products of the event. This is the simpler one, it basically + # looks like the check-in counter above. pqs_all = OrderPosition.objects.filter( order__event=event, order__status=Order.STATUS_PAID, @@ -41,10 +57,16 @@ class CheckinList(LoggedModel): ).order_by().values('order__event').annotate( c=Count('*') ).values('c') + + # Now we need a subquery for the case of checkin lists that are limited to certain + # products. We cannot use OuterRef("limit_products") since that would do a cross-product + # with the products table and we'd get duplicate rows in the output with different annotations + # on them, which isn't useful at all. Therefore, we need to add a second layer of subqueries + # to retrieve all of those items and then check if the item_id is IN this subquery result. pqs_limited = OrderPosition.objects.filter( order__event=event, order__status=Order.STATUS_PAID, - item__in=OuterRef('limit_products') + item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk')) ).filter( # This assumes that in an event with subevents, *all* positions have subevents # and *all* checkin lists have a subevent assigned @@ -54,6 +76,9 @@ class CheckinList(LoggedModel): c=Count('*') ).values('c') + # Finally, we put all of this together. We force empty subquery aggregates to 0 by using Coalesce() + # and decide which subquery to use for this row. In the end, we compute an integer percentage in case + # we want to display a progress bar. return qs.annotate( checkin_count=Coalesce(Subquery(cqs, output_field=models.IntegerField()), 0), position_count=Coalesce(Case( diff --git a/src/pretix/control/views/dashboards.py b/src/pretix/control/views/dashboards.py index e8d4e30d56..a6c5646422 100644 --- a/src/pretix/control/views/dashboards.py +++ b/src/pretix/control/views/dashboards.py @@ -192,14 +192,6 @@ def shop_state_widget(sender, **kwargs): @receiver(signal=event_dashboard_widgets) def checkin_widget(sender, subevent=None, **kwargs): - size_qs = OrderPosition.objects.filter(order__event=sender, order__status='p') - checked_qs = OrderPosition.objects.filter(order__event=sender, order__status='p', checkins__isnull=False) - - # if this setting is False, we check only items for admission - if not sender.settings.ticket_download_nonadm: - size_qs = size_qs.filter(item__admission=True) - checked_qs = checked_qs.filter(item__admission=True) - widgets = [] qs = sender.checkin_lists.filter(subevent=subevent) qs = CheckinList.annotate_with_numbers(qs, sender) diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index 1ee20d2811..ead359c4f5 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -13,8 +13,9 @@ from django.test import TestCase from django.utils.timezone import now from pretix.base.models import ( - CachedFile, CartPosition, Event, Item, ItemCategory, ItemVariation, Order, - OrderPosition, Organizer, Question, Quota, User, Voucher, WaitingListEntry, + CachedFile, CartPosition, CheckinList, Event, Item, ItemCategory, + ItemVariation, Order, OrderPosition, Organizer, Question, Quota, User, + Voucher, WaitingListEntry, ) from pretix.base.models.event import SubEvent from pretix.base.models.items import SubEventItem, SubEventItemVariation @@ -1066,3 +1067,66 @@ class CachedFileTestCase(TestCase): assert f.read().strip() == "file_content" cf.delete() assert not default_storage.exists(cf.file.name) + + +class CheckinListTestCase(TestCase): + @classmethod + def setUpTestData(cls): + cls.organizer = Organizer.objects.create(name='Dummy', slug='dummy') + cls.event = Event.objects.create( + organizer=cls.organizer, name='Dummy', slug='dummy', + date_from=now(), date_to=now() - timedelta(hours=1), + ) + cls.item1 = cls.event.items.create(name="Ticket", default_price=12) + cls.item2 = cls.event.items.create(name="Shirt", default_price=6) + cls.cl_all = cls.event.checkin_lists.create( + name='All', all_products=True + ) + cls.cl_both = cls.event.checkin_lists.create( + name='Both', all_products=False + ) + cls.cl_both.limit_products.add(cls.item1) + cls.cl_both.limit_products.add(cls.item2) + cls.cl_tickets = cls.event.checkin_lists.create( + name='Tickets', all_products=False + ) + cls.cl_tickets.limit_products.add(cls.item1) + o = Order.objects.create( + code='FOO', event=cls.event, email='dummy@dummy.test', + status=Order.STATUS_PAID, + datetime=now(), expires=now() + timedelta(days=10), + total=Decimal("30"), payment_provider='banktransfer', locale='en' + ) + OrderPosition.objects.create( + order=o, + item=cls.item1, + variation=None, + price=Decimal("12"), + ) + op2 = OrderPosition.objects.create( + order=o, + item=cls.item1, + variation=None, + price=Decimal("12"), + ) + op3 = OrderPosition.objects.create( + order=o, + item=cls.item2, + variation=None, + price=Decimal("6"), + ) + op2.checkins.create(list=cls.cl_tickets) + op3.checkins.create(list=cls.cl_both) + + def test_annotated(self): + lists = list(CheckinList.annotate_with_numbers(self.event.checkin_lists.order_by('name'), self.event)) + assert lists == [self.cl_all, self.cl_both, self.cl_tickets] + assert lists[0].checkin_count == 0 + assert lists[0].position_count == 3 + assert lists[0].percent == 0 + assert lists[1].checkin_count == 1 + assert lists[1].position_count == 3 + assert lists[1].percent == 33 + assert lists[2].checkin_count == 1 + assert lists[2].position_count == 2 + assert lists[2].percent == 50