forked from CGM_Public/pretix_original
Fix annotated check-in list query
This commit is contained in:
@@ -17,7 +17,18 @@ class CheckinList(LoggedModel):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_with_numbers(qs, event):
|
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(
|
cqs = Checkin.objects.filter(
|
||||||
position__order__event=event,
|
position__order__event=event,
|
||||||
position__order__status=Order.STATUS_PAID,
|
position__order__status=Order.STATUS_PAID,
|
||||||
@@ -30,6 +41,11 @@ class CheckinList(LoggedModel):
|
|||||||
).order_by().values('list').annotate(
|
).order_by().values('list').annotate(
|
||||||
c=Count('*')
|
c=Count('*')
|
||||||
).values('c')
|
).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(
|
pqs_all = OrderPosition.objects.filter(
|
||||||
order__event=event,
|
order__event=event,
|
||||||
order__status=Order.STATUS_PAID,
|
order__status=Order.STATUS_PAID,
|
||||||
@@ -41,10 +57,16 @@ class CheckinList(LoggedModel):
|
|||||||
).order_by().values('order__event').annotate(
|
).order_by().values('order__event').annotate(
|
||||||
c=Count('*')
|
c=Count('*')
|
||||||
).values('c')
|
).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(
|
pqs_limited = OrderPosition.objects.filter(
|
||||||
order__event=event,
|
order__event=event,
|
||||||
order__status=Order.STATUS_PAID,
|
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(
|
).filter(
|
||||||
# This assumes that in an event with subevents, *all* positions have subevents
|
# This assumes that in an event with subevents, *all* positions have subevents
|
||||||
# and *all* checkin lists have a subevent assigned
|
# and *all* checkin lists have a subevent assigned
|
||||||
@@ -54,6 +76,9 @@ class CheckinList(LoggedModel):
|
|||||||
c=Count('*')
|
c=Count('*')
|
||||||
).values('c')
|
).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(
|
return qs.annotate(
|
||||||
checkin_count=Coalesce(Subquery(cqs, output_field=models.IntegerField()), 0),
|
checkin_count=Coalesce(Subquery(cqs, output_field=models.IntegerField()), 0),
|
||||||
position_count=Coalesce(Case(
|
position_count=Coalesce(Case(
|
||||||
|
|||||||
@@ -192,14 +192,6 @@ def shop_state_widget(sender, **kwargs):
|
|||||||
|
|
||||||
@receiver(signal=event_dashboard_widgets)
|
@receiver(signal=event_dashboard_widgets)
|
||||||
def checkin_widget(sender, subevent=None, **kwargs):
|
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 = []
|
widgets = []
|
||||||
qs = sender.checkin_lists.filter(subevent=subevent)
|
qs = sender.checkin_lists.filter(subevent=subevent)
|
||||||
qs = CheckinList.annotate_with_numbers(qs, sender)
|
qs = CheckinList.annotate_with_numbers(qs, sender)
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ from django.test import TestCase
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
CachedFile, CartPosition, Event, Item, ItemCategory, ItemVariation, Order,
|
CachedFile, CartPosition, CheckinList, Event, Item, ItemCategory,
|
||||||
OrderPosition, Organizer, Question, Quota, User, Voucher, WaitingListEntry,
|
ItemVariation, Order, OrderPosition, Organizer, Question, Quota, User,
|
||||||
|
Voucher, WaitingListEntry,
|
||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||||
@@ -1066,3 +1067,66 @@ class CachedFileTestCase(TestCase):
|
|||||||
assert f.read().strip() == "file_content"
|
assert f.read().strip() == "file_content"
|
||||||
cf.delete()
|
cf.delete()
|
||||||
assert not default_storage.exists(cf.file.name)
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user