forked from CGM_Public/pretix_original
Fix performance and logic issues in auto-exit-all
This commit is contained in:
@@ -35,10 +35,11 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import connection, models
|
from django.db import models
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, Max, OuterRef, Q, Subquery, Value, Window,
|
Count, Exists, F, Max, OuterRef, Q, Subquery, Value, Window,
|
||||||
)
|
)
|
||||||
|
from django.db.models.expressions import RawSQL
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
@@ -98,15 +99,18 @@ class CheckinList(LoggedModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('subevent__date_from', 'name')
|
ordering = ('subevent__date_from', 'name')
|
||||||
|
|
||||||
@property
|
def positions_query(self, ignore_status=False):
|
||||||
def positions(self):
|
|
||||||
from . import Order, OrderPosition
|
from . import Order, OrderPosition
|
||||||
|
|
||||||
qs = OrderPosition.objects.filter(
|
qs = OrderPosition.all.filter(
|
||||||
order__event=self.event,
|
order__event=self.event,
|
||||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [
|
|
||||||
Order.STATUS_PAID],
|
|
||||||
)
|
)
|
||||||
|
if not ignore_status:
|
||||||
|
qs = qs.filter(
|
||||||
|
canceled=False,
|
||||||
|
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.include_pending else [Order.STATUS_PAID],
|
||||||
|
)
|
||||||
|
|
||||||
if self.subevent_id:
|
if self.subevent_id:
|
||||||
qs = qs.filter(subevent_id=self.subevent_id)
|
qs = qs.filter(subevent_id=self.subevent_id)
|
||||||
if not self.all_products:
|
if not self.all_products:
|
||||||
@@ -114,10 +118,22 @@ class CheckinList(LoggedModel):
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def positions_inside(self):
|
def positions(self):
|
||||||
return self.positions.annotate(
|
return self.positions_query(ignore_status=True)
|
||||||
|
|
||||||
|
@scopes_disabled()
|
||||||
|
def positions_inside_query(self, ignore_status=False, at_time=None):
|
||||||
|
if at_time is None:
|
||||||
|
c_q = []
|
||||||
|
else:
|
||||||
|
c_q = [Q(datetime__lt=at_time)]
|
||||||
|
|
||||||
|
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
|
||||||
|
# Use a simple approach that works on all databases
|
||||||
|
qs = self.positions_query(ignore_status=ignore_status).annotate(
|
||||||
last_entry=Subquery(
|
last_entry=Subquery(
|
||||||
Checkin.objects.filter(
|
Checkin.objects.filter(
|
||||||
|
*c_q,
|
||||||
position_id=OuterRef('pk'),
|
position_id=OuterRef('pk'),
|
||||||
list_id=self.pk,
|
list_id=self.pk,
|
||||||
type=Checkin.TYPE_ENTRY,
|
type=Checkin.TYPE_ENTRY,
|
||||||
@@ -127,6 +143,7 @@ class CheckinList(LoggedModel):
|
|||||||
),
|
),
|
||||||
last_exit=Subquery(
|
last_exit=Subquery(
|
||||||
Checkin.objects.filter(
|
Checkin.objects.filter(
|
||||||
|
*c_q,
|
||||||
position_id=OuterRef('pk'),
|
position_id=OuterRef('pk'),
|
||||||
list_id=self.pk,
|
list_id=self.pk,
|
||||||
type=Checkin.TYPE_EXIT,
|
type=Checkin.TYPE_EXIT,
|
||||||
@@ -140,12 +157,7 @@ class CheckinList(LoggedModel):
|
|||||||
Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry'))
|
Q(last_exit__isnull=True) | Q(last_exit__lt=F('last_entry'))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
return qs
|
||||||
@property
|
|
||||||
def inside_count(self):
|
|
||||||
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
|
|
||||||
# Use the simple query that works on all databases
|
|
||||||
return self.positions_inside.count()
|
|
||||||
|
|
||||||
# Use the PostgreSQL-specific query using Window functions, which is a lot faster.
|
# Use the PostgreSQL-specific query using Window functions, which is a lot faster.
|
||||||
# On a real-world example with ~100k tickets, of which ~17k are checked in, we observed
|
# On a real-world example with ~100k tickets, of which ~17k are checked in, we observed
|
||||||
@@ -157,7 +169,7 @@ class CheckinList(LoggedModel):
|
|||||||
# dedupliate by position and count it up.
|
# dedupliate by position and count it up.
|
||||||
cl = self
|
cl = self
|
||||||
base_q, base_params = (
|
base_q, base_params = (
|
||||||
Checkin.all.filter(successful=True, position__in=cl.positions, list=cl)
|
Checkin.all.filter(*c_q, successful=True, list=cl)
|
||||||
.annotate(
|
.annotate(
|
||||||
cnt_exists_after=Window(
|
cnt_exists_after=Window(
|
||||||
expression=Count("position_id", filter=Q(type=Value("exit"))),
|
expression=Count("position_id", filter=Q(type=Value("exit"))),
|
||||||
@@ -171,26 +183,25 @@ class CheckinList(LoggedModel):
|
|||||||
.values("position_id", "type", "datetime", "cnt_exists_after")
|
.values("position_id", "type", "datetime", "cnt_exists_after")
|
||||||
.query.sql_with_params()
|
.query.sql_with_params()
|
||||||
)
|
)
|
||||||
|
return self.positions.filter(
|
||||||
with connection.cursor() as cursor:
|
pk__in=RawSQL(
|
||||||
cursor.execute(
|
|
||||||
f"""
|
f"""
|
||||||
SELECT COUNT(*) FROM (
|
SELECT "position_id"
|
||||||
SELECT COUNT("position_id")
|
|
||||||
FROM ({str(base_q)}) s
|
FROM ({str(base_q)}) s
|
||||||
WHERE "type" = %s AND "cnt_exists_after" = 0
|
WHERE "type" = %s AND "cnt_exists_after" = 0
|
||||||
GROUP BY "position_id"
|
GROUP BY "position_id"
|
||||||
) a;
|
|
||||||
""",
|
""",
|
||||||
[
|
[*base_params, Checkin.TYPE_ENTRY]
|
||||||
*base_params,
|
|
||||||
"entry",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
rows = cursor.fetchall()
|
)
|
||||||
if rows:
|
|
||||||
return rows[0][0]
|
@property
|
||||||
return 0
|
def positions_inside(self):
|
||||||
|
return self.positions_inside_query(None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inside_count(self):
|
||||||
|
return self.positions_inside_query(None).count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@scopes_disabled()
|
@scopes_disabled()
|
||||||
|
|||||||
@@ -842,10 +842,7 @@ def process_exit_all(sender, **kwargs):
|
|||||||
exit_all_at__isnull=False
|
exit_all_at__isnull=False
|
||||||
).select_related('event', 'event__organizer')
|
).select_related('event', 'event__organizer')
|
||||||
for cl in qs:
|
for cl in qs:
|
||||||
positions = cl.positions_inside.filter(
|
positions = cl.positions_inside_query(ignore_status=True, at_time=cl.exit_all_at)
|
||||||
Q(last_exit__isnull=True) | Q(last_exit__lte=cl.exit_all_at),
|
|
||||||
last_entry__lte=cl.exit_all_at,
|
|
||||||
)
|
|
||||||
for p in positions:
|
for p in positions:
|
||||||
with scope(organizer=cl.event.organizer):
|
with scope(organizer=cl.event.organizer):
|
||||||
ci = Checkin.objects.create(
|
ci = Checkin.objects.create(
|
||||||
|
|||||||
Reference in New Issue
Block a user