Allow to sort export of all tickets or badges by question answer (#2865)

This commit is contained in:
Raphael Michel
2022-10-26 10:43:13 +02:00
committed by GitHub
parent f2c72e5ff8
commit 89dda69205
2 changed files with 153 additions and 27 deletions

View File

@@ -32,6 +32,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import logging
from collections import OrderedDict
from datetime import datetime, time, timedelta
from io import BytesIO
@@ -39,20 +40,26 @@ from io import BytesIO
import dateutil.parser
from django import forms
from django.core.files.base import ContentFile
from django.db.models import Q
from django.db.models.functions import Coalesce
from django.db import DataError, models
from django.db.models import OuterRef, Q, Subquery
from django.db.models.functions import Cast, Coalesce
from django.utils.timezone import make_aware
from django.utils.translation import gettext as _, gettext_lazy
from PyPDF2 import PdfMerger
from pretix.base.exporter import BaseExporter
from pretix.base.i18n import language
from pretix.base.models import Event, Order, OrderPosition
from pretix.base.models import (
Event, Order, OrderPosition, Question, QuestionAnswer,
)
from pretix.base.settings import PERSON_NAME_SCHEMES
from ...base.services.export import ExportError
from ...helpers.templatetags.jsonfield import JSONExtract
from .ticketoutput import PdfTicketOutput
logger = logging.getLogger(__name__)
class AllTicketsPDF(BaseExporter):
name = "alltickets"
@@ -93,7 +100,16 @@ class AllTicketsPDF(BaseExporter):
] + ([
('name:{}'.format(k), _('Attendee name: {part}').format(part=label))
for k, label, w in name_scheme['fields']
] if name_scheme and len(name_scheme['fields']) > 1 else []),
] if name_scheme and len(name_scheme['fields']) > 1 else []) + ([
('question:{}'.format(q.identifier), _('Question: {question}').format(question=q.question))
for q in self.event.questions.filter(type__in=(
# All except TYPE_FILE and future ones
Question.TYPE_TIME, Question.TYPE_TEXT, Question.TYPE_DATE, Question.TYPE_BOOLEAN,
Question.TYPE_COUNTRYCODE, Question.TYPE_DATETIME, Question.TYPE_NUMBER,
Question.TYPE_PHONENUMBER, Question.TYPE_STRING, Question.TYPE_CHOICE,
Question.TYPE_CHOICE_MULTIPLE
))
] if not self.is_multievent else [])
)),
]
)
@@ -146,30 +162,78 @@ class AllTicketsPDF(BaseExporter):
).order_by(
'resolved_name_part'
)
elif form_data.get('order_by', '').startswith('question:'):
part = form_data['order_by'].split(':', 1)[1]
question = self.event.questions.get(identifier=part)
if question.type == Question.TYPE_NUMBER:
# We use a database-level type cast to sort numbers like 1, 2, 10, 11 and not like 1, 10, 11, 2.
# This works perfectly fine e.g. on SQLite where an invalid number will be casted to 0, but will
# raise a DataError on PostgreSQL if there is a non-number in the data.
question_subquery = Subquery(
QuestionAnswer.objects.filter(
orderposition_id=OuterRef('pk'),
question_id=question.pk,
).annotate(
converted_answer=Cast('answer', output_field=models.FloatField())
).order_by().values('converted_answer')[:1]
)
elif question.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
# Sorting by choice questions must be handled differently because the QuestionAnswer.value
# attribute may be dependent on the submitters locale, which we don't want here. So we sort by
# order of the position instead. In case of multiple choice, the first selected order counts, which
# is not perfect but better than no sorting at all.
question_subquery = Subquery(
QuestionAnswer.options.through.objects.filter(
questionanswer__orderposition_id=OuterRef('pk'),
questionanswer__question_id=question.pk,
).order_by('questionoption__position').values('questionoption__position')[:1]
)
else:
# For all other types, we just sort by treating the answer field as a string. This works fine for
# all string-y types including dates and date-times (due to ISO 8601 format), country codes, etc
question_subquery = Subquery(
QuestionAnswer.objects.filter(
orderposition_id=OuterRef('pk'),
question_id=question.pk,
).order_by().values('answer')[:1]
)
qs = qs.annotate(
question_answer=question_subquery,
).order_by(
'question_answer'
)
o = PdfTicketOutput(Event.objects.none())
for op in qs:
if not op.generate_ticket:
continue
try:
for op in qs:
if not op.generate_ticket:
continue
if op.order.event != o.event:
o = PdfTicketOutput(op.event)
if op.order.event != o.event:
o = PdfTicketOutput(op.event)
with language(op.order.locale, o.event.settings.region):
layout = o.layout_map.get(
(op.item_id, op.order.sales_channel),
o.layout_map.get(
(op.item_id, 'web'),
o.default_layout
with language(op.order.locale, o.event.settings.region):
layout = o.layout_map.get(
(op.item_id, op.order.sales_channel),
o.layout_map.get(
(op.item_id, 'web'),
o.default_layout
)
)
)
outbuffer = o._draw_page(layout, op, op.order)
merger.append(ContentFile(outbuffer.read()))
outbuffer = o._draw_page(layout, op, op.order)
merger.append(ContentFile(outbuffer.read()))
outbuffer = BytesIO()
merger.write(outbuffer)
merger.close()
outbuffer.seek(0)
outbuffer = BytesIO()
merger.write(outbuffer)
merger.close()
outbuffer.seek(0)
except DataError:
logging.exception('DataError during export')
raise ExportError(
_('Your data could not be converted as requested. This could be caused by invalid values in your '
'databases, such as answers to number questions which are not a number.')
)
if self.is_multievent:
return '{}_tickets.pdf'.format(self.events.first().organizer.slug), 'application/pdf', outbuffer.read()