diff --git a/src/pretix/helpers/__init__.py b/src/pretix/helpers/__init__.py index 2ead6c5a0..d6c8db352 100644 --- a/src/pretix/helpers/__init__.py +++ b/src/pretix/helpers/__init__.py @@ -1,5 +1,7 @@ from django.apps import AppConfig +from .database import * # noqa + class PretixHelpersConfig(AppConfig): name = 'pretix.helpers' diff --git a/src/pretix/helpers/database.py b/src/pretix/helpers/database.py index fe734ae47..764f1162b 100644 --- a/src/pretix/helpers/database.py +++ b/src/pretix/helpers/database.py @@ -1,7 +1,7 @@ import contextlib from django.db import transaction -from django.db.models import Aggregate +from django.db.models import Aggregate, Field, Lookup from django.db.models.expressions import OrderBy @@ -94,3 +94,14 @@ class ReplicaRouter: def allow_migrate(self, db, app_label, model_name=None, **hintrs): return True + + +@Field.register_lookup +class NotEqual(Lookup): + lookup_name = 'ne' + + def as_sql(self, compiler, connection): + lhs, lhs_params = self.process_lhs(compiler, connection) + rhs, rhs_params = self.process_rhs(compiler, connection) + params = lhs_params + rhs_params + return '%s <> %s' % (lhs, rhs), params diff --git a/src/pretix/plugins/checkinlists/exporters.py b/src/pretix/plugins/checkinlists/exporters.py index 0292d3fab..3a3eb650d 100644 --- a/src/pretix/plugins/checkinlists/exporters.py +++ b/src/pretix/plugins/checkinlists/exporters.py @@ -3,8 +3,8 @@ from collections import OrderedDict import dateutil.parser from django import forms from django.conf import settings -from django.db.models import Exists, Max, OuterRef, Subquery -from django.db.models.functions import Coalesce +from django.db.models import Case, Exists, Max, OuterRef, Subquery, Value, When +from django.db.models.functions import Coalesce, NullIf from django.urls import reverse from django.utils.formats import date_format from django.utils.timezone import is_aware, make_aware @@ -112,14 +112,24 @@ class CheckInListMixin(BaseExporter): qs = qs.filter(subevent=cl.subevent) if form_data['sort'] == 'name': - qs = qs.order_by(Coalesce('attendee_name_cached', 'addon_to__attendee_name_cached', 'order__invoice_address__name_cached'), - 'order__code') + qs = qs.order_by( + Coalesce( + NullIf('attendee_name_cached', Value('')), + NullIf('addon_to__attendee_name_cached', Value('')), + NullIf('order__invoice_address__name_cached', Value('')), + 'order__code' + ) + ) elif form_data['sort'] == 'code': qs = qs.order_by('order__code') elif form_data['sort'].startswith('name:'): part = form_data['sort'][5:] qs = qs.annotate( - resolved_name=Coalesce('attendee_name_parts', 'addon_to__attendee_name_parts', 'order__invoice_address__name_parts') + resolved_name=Case( + When(attendee_name_cached__ne='', then='attendee_name_parts'), + When(addon_to__attendee_name_cached__isnull=False, addon_to__attendee_name_cached__ne='', then='addon_to__attendee_name_parts'), + default='order__invoice_address__name_parts', + ) ).annotate( resolved_name_part=JSONExtract('resolved_name', part) ).order_by( diff --git a/src/tests/plugins/test_checkinlist.py b/src/tests/plugins/test_checkinlist.py index 968379076..c828d46e4 100644 --- a/src/tests/plugins/test_checkinlist.py +++ b/src/tests/plugins/test_checkinlist.py @@ -6,7 +6,9 @@ import pytz from django.utils.timezone import now from django_scopes import scope -from pretix.base.models import Event, Item, Order, OrderPosition, Organizer +from pretix.base.models import ( + Event, InvoiceAddress, Item, Order, OrderPosition, Organizer, +) from pretix.plugins.checkinlists.exporters import CSVCheckinList @@ -114,3 +116,89 @@ def test_csv_order_by_name_parts(event): # noqa "FOO","Mrs Andrea J Zulu","Mrs","Andrea","J","Zulu","Ticket","13.00","","No","ggsngqtnmhx74jswjngw3fk8pfwz2a7k", "dummy@dummy.test","","","2019-02-22","No","" """) + + +@pytest.mark.django_db +def test_csv_order_by_inherited_name_parts(event): # noqa + from django.conf import settings + if not settings.JSON_FIELD_AVAILABLE: + raise pytest.skip("Not supported on this database") + + with scope(organizer=event.organizer): + OrderPosition.objects.filter(attendee_name_cached__icontains="Andrea").delete() + op = OrderPosition.objects.get() + op.attendee_name_parts = {} + op.save() + order2 = Order.objects.create( + code='BAR', event=event, email='dummy@dummy.test', + status=Order.STATUS_PAID, + datetime=datetime.datetime(2019, 2, 22, 14, 0, 0, tzinfo=pytz.UTC), expires=now() + datetime.timedelta(days=10), + total=33, locale='en' + ) + OrderPosition.objects.create( + order=order2, + item=event.items.first(), + variation=None, + price=Decimal("23"), + secret='hutjztuxhkbtwnesv2suqv26k6ttytyy' + ) + InvoiceAddress.objects.create( + order=event.orders.get(code='BAR'), + name_parts={"title": "Mr", "given_name": "Albert", "middle_name": "J", "family_name": "Zulu", "_scheme": "title_given_middle_family"} + ) + InvoiceAddress.objects.create( + order=event.orders.get(code='FOO'), + name_parts={"title": "Mr", "given_name": "Paul", "middle_name": "A", "family_name": "Jones", "_scheme": "title_given_middle_family"} + ) + + c = CSVCheckinList(event) + _, _, content = c.render({ + 'list': event.checkin_lists.first().pk, + 'secrets': True, + 'sort': 'name', + '_format': 'default', + 'questions': [] + }) + assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title", +"Attendee name: First name","Attendee name: Middle name","Attendee name: Family name","Product","Price", +"Checked in","Automatically checked in","Secret","E-mail","Company","Voucher code","Order date","Requires special + attention","Comment" +"BAR","Mr Albert J Zulu","Mr","Albert","J","Zulu","Ticket","23.00","","No","hutjztuxhkbtwnesv2suqv26k6ttytyy", +"dummy@dummy.test","","","2019-02-22","No","" +"FOO","Mr Paul A Jones","Mr","Paul","A","Jones","Ticket","23.00","","No","hutjztuxhkbtwnesv2suqv26k6ttytxx", +"dummy@dummy.test","","","2019-02-22","No","" +""") + c = CSVCheckinList(event) + _, _, content = c.render({ + 'list': event.checkin_lists.first().pk, + 'secrets': True, + 'sort': 'name:given_name', + '_format': 'default', + 'questions': [] + }) + assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title", +"Attendee name: First name","Attendee name: Middle name","Attendee name: Family name","Product","Price", +"Checked in","Automatically checked in","Secret","E-mail","Company","Voucher code","Order date","Requires special + attention","Comment" +"BAR","Mr Albert J Zulu","Mr","Albert","J","Zulu","Ticket","23.00","","No","hutjztuxhkbtwnesv2suqv26k6ttytyy", +"dummy@dummy.test","","","2019-02-22","No","" +"FOO","Mr Paul A Jones","Mr","Paul","A","Jones","Ticket","23.00","","No","hutjztuxhkbtwnesv2suqv26k6ttytxx", +"dummy@dummy.test","","","2019-02-22","No","" +""") + c = CSVCheckinList(event) + _, _, content = c.render({ + 'list': event.checkin_lists.first().pk, + 'secrets': True, + 'sort': 'name:family_name', + '_format': 'default', + 'questions': [] + }) + assert clean(content.decode()) == clean(""""Order code","Attendee name","Attendee name: Title", +"Attendee name: First name","Attendee name: Middle name","Attendee name: Family name","Product","Price", +"Checked in","Automatically checked in","Secret","E-mail","Company","Voucher code","Order date","Requires special + attention","Comment" +"FOO","Mr Paul A Jones","Mr","Paul","A","Jones","Ticket","23.00","","No","hutjztuxhkbtwnesv2suqv26k6ttytxx", +"dummy@dummy.test","","","2019-02-22","No","" +"BAR","Mr Albert J Zulu","Mr","Albert","J","Zulu","Ticket","23.00","","No","hutjztuxhkbtwnesv2suqv26k6ttytyy", +"dummy@dummy.test","","","2019-02-22","No","" +""")