forked from CGM_Public/pretix_original
Improve performance of global order search
This commit is contained in:
39
src/pretix/base/migrations/0079_auto_20180115_0855.py
Normal file
39
src/pretix/base/migrations/0079_auto_20180115_0855.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.8 on 2018-01-15 08:55
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.models import F
|
||||||
|
from django.db.models.functions import Concat
|
||||||
|
|
||||||
|
|
||||||
|
def set_full_invoice_no(app, schema_editor):
|
||||||
|
Invoice = app.get_model('pretixbase', 'Invoice')
|
||||||
|
Invoice.objects.all().update(
|
||||||
|
full_invoice_no=Concat(F('prefix'), F('invoice_no'))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0078_auto_20171206_1603'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='full_invoice_no',
|
||||||
|
field=models.CharField(db_index=True, default='', max_length=190),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='question',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'), ('C', 'Choose one from a list'), ('M', 'Choose multiple from a list'), ('F', 'File upload'), ('D', 'Date'), ('H', 'Time'), ('W', 'Date and time')], max_length=5, verbose_name='Question type'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
set_full_invoice_no,
|
||||||
|
migrations.RunPython.noop
|
||||||
|
)
|
||||||
|
]
|
||||||
@@ -41,6 +41,8 @@ class Invoice(models.Model):
|
|||||||
:type invoice_from: str
|
:type invoice_from: str
|
||||||
:param invoice_to: The receiver address
|
:param invoice_to: The receiver address
|
||||||
:type invoice_to: str
|
:type invoice_to: str
|
||||||
|
:param full_invoice_no: The full invoice number (for performance reasons only)
|
||||||
|
:type full_invoice_no: str
|
||||||
:param date: The invoice date
|
:param date: The invoice date
|
||||||
:type date: date
|
:type date: date
|
||||||
:param locale: The locale in which the invoice should be printed
|
:param locale: The locale in which the invoice should be printed
|
||||||
@@ -67,6 +69,7 @@ class Invoice(models.Model):
|
|||||||
event = models.ForeignKey('Event', related_name='invoices', db_index=True)
|
event = models.ForeignKey('Event', related_name='invoices', db_index=True)
|
||||||
prefix = models.CharField(max_length=160, db_index=True)
|
prefix = models.CharField(max_length=160, db_index=True)
|
||||||
invoice_no = models.CharField(max_length=19, db_index=True)
|
invoice_no = models.CharField(max_length=19, db_index=True)
|
||||||
|
full_invoice_no = models.CharField(max_length=190, db_index=True)
|
||||||
is_cancellation = models.BooleanField(default=False)
|
is_cancellation = models.BooleanField(default=False)
|
||||||
refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True)
|
refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True)
|
||||||
invoice_from = models.TextField()
|
invoice_from = models.TextField()
|
||||||
@@ -122,6 +125,8 @@ class Invoice(models.Model):
|
|||||||
# Suppress duplicate key errors and try again
|
# Suppress duplicate key errors and try again
|
||||||
if i == 9:
|
if i == 9:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
self.full_invoice_no = self.prefix + self.invoice_no
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db.models import F, Q
|
from django.db.models import Exists, F, OuterRef, Q
|
||||||
from django.db.models.functions import Coalesce, Concat
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import Event, Invoice, Item, Order, Organizer, SubEvent
|
from pretix.base.models import (
|
||||||
|
Event, Invoice, Item, Order, OrderPosition, Organizer, SubEvent,
|
||||||
|
)
|
||||||
from pretix.base.signals import register_payment_providers
|
from pretix.base.signals import register_payment_providers
|
||||||
from pretix.control.utils.i18n import i18ncomp
|
from pretix.control.utils.i18n import i18ncomp
|
||||||
from pretix.helpers.database import FixedOrderBy, rolledback_transaction
|
from pretix.helpers.database import FixedOrderBy, rolledback_transaction
|
||||||
@@ -115,22 +117,25 @@ class OrderFilterForm(FilterForm):
|
|||||||
else:
|
else:
|
||||||
code = Q(code__icontains=Order.normalize_code(u))
|
code = Q(code__icontains=Order.normalize_code(u))
|
||||||
|
|
||||||
matching_invoices = Invoice.objects.annotate(
|
matching_invoices = Invoice.objects.filter(
|
||||||
inr=Concat('prefix', 'invoice_no')
|
|
||||||
).filter(
|
|
||||||
Q(invoice_no__iexact=u)
|
Q(invoice_no__iexact=u)
|
||||||
| Q(invoice_no__iexact=u.zfill(5))
|
| Q(invoice_no__iexact=u.zfill(5))
|
||||||
| Q(inr=u)
|
| Q(full_invoice_no__iexact=u)
|
||||||
).values_list('order_id', flat=True)
|
).values_list('order_id', flat=True)
|
||||||
|
|
||||||
qs = qs.filter(
|
matching_positions = OrderPosition.objects.filter(
|
||||||
|
Q(order=OuterRef('pk')) & Q(
|
||||||
|
Q(attendee_name__icontains=u) & Q(attendee_email__icontains=u)
|
||||||
|
)
|
||||||
|
).values('id')
|
||||||
|
|
||||||
|
qs = qs.annotate(has_pos=Exists(matching_positions)).filter(
|
||||||
code
|
code
|
||||||
| Q(email__icontains=u)
|
| Q(email__icontains=u)
|
||||||
| Q(positions__attendee_name__icontains=u)
|
|
||||||
| Q(positions__attendee_email__icontains=u)
|
|
||||||
| Q(invoice_address__name__icontains=u)
|
| Q(invoice_address__name__icontains=u)
|
||||||
| Q(invoice_address__company__icontains=u)
|
| Q(invoice_address__company__icontains=u)
|
||||||
| Q(pk__in=matching_invoices)
|
| Q(pk__in=matching_invoices)
|
||||||
|
| Q(has_pos=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
if fdata.get('status'):
|
if fdata.get('status'):
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load urlreplace %}
|
||||||
|
<nav class="text-center pagination-container">
|
||||||
|
<ul class="pagination">
|
||||||
|
{% if is_paginated %}
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li>
|
||||||
|
<a href="?{% url_replace request 'page' page_obj.previous_page_number %}">
|
||||||
|
<span>«</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="page-current"><a>
|
||||||
|
{% blocktrans trimmed with page=page_obj.number of=page_obj.paginator.num_pages count=page_obj.paginator.count %}
|
||||||
|
Page {{ page }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
</a></li>
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li>
|
||||||
|
<a href="?{% url_replace request 'page' page_obj.next_page_number %}">
|
||||||
|
<span>»</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if page_obj.paginator.count > 1 %}
|
||||||
|
<li class="page-current"><a>
|
||||||
|
{% blocktrans trimmed with count=page_obj.paginator.count %}
|
||||||
|
{{ count }} elements
|
||||||
|
{% endblocktrans %}
|
||||||
|
</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% if page_size %}
|
||||||
|
<div class="clearfix">
|
||||||
|
<small>
|
||||||
|
{% trans "Show per page:" %}
|
||||||
|
</small>
|
||||||
|
<a href="?{% url_replace request "page_size" "25" "page" "1" %}">
|
||||||
|
{% if page_size == 25 %}<strong>{% endif %}25{% if page_size == 25 %}</strong>{% endif %}</a> |
|
||||||
|
<a href="?{% url_replace request "page_size" "50" "page" "1" %}">
|
||||||
|
{% if page_size == 50 %}<strong>{% endif %}50{% if page_size == 50 %}</strong>{% endif %}</a> |
|
||||||
|
<a href="?{% url_replace request "page_size" "100" "page" "1" %}">
|
||||||
|
{% if page_size == 100 %}<strong>{% endif %}100{% if page_size == 100 %}</strong>{% endif %}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
@@ -47,9 +47,6 @@
|
|||||||
<th class="text-right">{% trans "Order total" %}
|
<th class="text-right">{% trans "Order total" %}
|
||||||
<a href="?{% url_replace request 'ordering' '-total' %}"><i class="fa fa-caret-down"></i></a>
|
<a href="?{% url_replace request 'ordering' '-total' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<a href="?{% url_replace request 'ordering' 'total' %}"><i class="fa fa-caret-up"></i></a></th>
|
<a href="?{% url_replace request 'ordering' 'total' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||||
<th class="text-right">{% trans "Positions" %}
|
|
||||||
<a href="?{% url_replace request 'ordering' '-pcnt' %}"><i class="fa fa-caret-down"></i></a>
|
|
||||||
<a href="?{% url_replace request 'ordering' 'pcnt' %}"><i class="fa fa-caret-up"></i></a></th>
|
|
||||||
<th class="text-right">{% trans "Status" %}
|
<th class="text-right">{% trans "Status" %}
|
||||||
<a href="?{% url_replace request 'ordering' '-status' %}"><i class="fa fa-caret-down"></i></a>
|
<a href="?{% url_replace request 'ordering' '-status' %}"><i class="fa fa-caret-down"></i></a>
|
||||||
<a href="?{% url_replace request 'ordering' 'status' %}"><i class="fa fa-caret-up"></i></a></th>
|
<a href="?{% url_replace request 'ordering' 'status' %}"><i class="fa fa-caret-up"></i></a></th>
|
||||||
@@ -74,7 +71,6 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
|
<td>{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||||
<td class="text-right">{{ o.total|floatformat:2 }} {{ o.event.currency }}</td>
|
<td class="text-right">{{ o.total|floatformat:2 }} {{ o.event.currency }}</td>
|
||||||
<td class="text-right">{{ o.pcnt }}</td>
|
|
||||||
<td class="text-right">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
<td class="text-right">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
@@ -87,5 +83,5 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% include "pretixcontrol/pagination.html" %}
|
{% include "pretixcontrol/pagination_huge.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import collections
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from django.core.paginator import (
|
||||||
|
EmptyPage, PageNotAnInteger, UnorderedObjectListWarning,
|
||||||
|
)
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.generic import edit
|
from django.views.generic import edit
|
||||||
|
|
||||||
|
|
||||||
@@ -56,3 +63,122 @@ class PaginationMixin:
|
|||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['page_size'] = self.get_paginate_by(None)
|
ctx['page_size'] = self.get_paginate_by(None)
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
class LargeResultSetPage(collections.Sequence):
|
||||||
|
|
||||||
|
def __init__(self, object_list, number, paginator):
|
||||||
|
self.object_list = object_list
|
||||||
|
self.number = number
|
||||||
|
self.paginator = paginator
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<Page %s>' % self.number
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.object_list)
|
||||||
|
|
||||||
|
def __getitem__(self, index):
|
||||||
|
if not isinstance(index, (slice, int)):
|
||||||
|
raise TypeError
|
||||||
|
# The object_list is converted to a list so that if it was a QuerySet
|
||||||
|
# it won't be a database hit per __getitem__.
|
||||||
|
if not isinstance(self.object_list, list):
|
||||||
|
self.object_list = list(self.object_list)
|
||||||
|
return self.object_list[index]
|
||||||
|
|
||||||
|
def has_next(self):
|
||||||
|
try:
|
||||||
|
return self[self.paginator.per_page - 1]
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_previous(self):
|
||||||
|
return self.number > 1
|
||||||
|
|
||||||
|
def has_other_pages(self):
|
||||||
|
return self.has_previous() or self.has_next()
|
||||||
|
|
||||||
|
def next_page_number(self):
|
||||||
|
return self.paginator.validate_number(self.number + 1)
|
||||||
|
|
||||||
|
def previous_page_number(self):
|
||||||
|
return self.paginator.validate_number(self.number - 1)
|
||||||
|
|
||||||
|
def start_index(self):
|
||||||
|
"""
|
||||||
|
Returns the 1-based index of the first object on this page,
|
||||||
|
relative to total objects in the paginator.
|
||||||
|
"""
|
||||||
|
# Special case, return zero if no items.
|
||||||
|
if self.paginator.count == 0:
|
||||||
|
return 0
|
||||||
|
return (self.paginator.per_page * (self.number - 1)) + 1
|
||||||
|
|
||||||
|
def end_index(self):
|
||||||
|
"""
|
||||||
|
Returns the 1-based index of the last object on this page,
|
||||||
|
relative to total objects found (hits).
|
||||||
|
"""
|
||||||
|
# Special case for the last page because there can be orphans.
|
||||||
|
if self.number == self.paginator.num_pages:
|
||||||
|
return self.paginator.count
|
||||||
|
return self.number * self.paginator.per_page
|
||||||
|
|
||||||
|
|
||||||
|
class LargeResultSetPaginator(object):
|
||||||
|
|
||||||
|
def __init__(self, object_list, per_page, orphans=0,
|
||||||
|
allow_empty_first_page=True):
|
||||||
|
self.object_list = object_list
|
||||||
|
self._check_object_list_is_ordered()
|
||||||
|
self.per_page = int(per_page)
|
||||||
|
self.orphans = int(orphans)
|
||||||
|
|
||||||
|
def validate_number(self, number):
|
||||||
|
"""
|
||||||
|
Validates the given 1-based page number.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
number = int(number)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise PageNotAnInteger(_('That page number is not an integer'))
|
||||||
|
if number < 1:
|
||||||
|
raise EmptyPage(_('That page number is less than 1'))
|
||||||
|
return number
|
||||||
|
|
||||||
|
def page(self, number):
|
||||||
|
"""
|
||||||
|
Returns a Page object for the given 1-based page number.
|
||||||
|
"""
|
||||||
|
number = self.validate_number(number)
|
||||||
|
bottom = (number - 1) * self.per_page
|
||||||
|
top = bottom + self.per_page
|
||||||
|
return self._get_page(self.object_list[bottom:top], number, self)
|
||||||
|
|
||||||
|
def _get_page(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns an instance of a single page.
|
||||||
|
|
||||||
|
This hook can be used by subclasses to use an alternative to the
|
||||||
|
standard :cls:`Page` object.
|
||||||
|
"""
|
||||||
|
return LargeResultSetPage(*args, **kwargs)
|
||||||
|
|
||||||
|
def _check_object_list_is_ordered(self):
|
||||||
|
"""
|
||||||
|
Warn if self.object_list is unordered (typically a QuerySet).
|
||||||
|
"""
|
||||||
|
ordered = getattr(self.object_list, 'ordered', None)
|
||||||
|
if ordered is not None and not ordered:
|
||||||
|
obj_list_repr = (
|
||||||
|
'{} {}'.format(self.object_list.model, self.object_list.__class__.__name__)
|
||||||
|
if hasattr(self.object_list, 'model')
|
||||||
|
else '{!r}'.format(self.object_list)
|
||||||
|
)
|
||||||
|
warnings.warn(
|
||||||
|
'Pagination may yield inconsistent results with an unordered '
|
||||||
|
'object_list: {}.'.format(obj_list_repr),
|
||||||
|
UnorderedObjectListWarning,
|
||||||
|
stacklevel=3
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
from django.db.models import Count, Q
|
from django.db.models import Q
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
|
|
||||||
from pretix.base.models import Order
|
from pretix.base.models import Order
|
||||||
from pretix.control.forms.filter import OrderSearchFilterForm
|
from pretix.control.forms.filter import OrderSearchFilterForm
|
||||||
from pretix.control.views import PaginationMixin
|
from pretix.control.views import LargeResultSetPaginator, PaginationMixin
|
||||||
|
|
||||||
|
|
||||||
class OrderSearch(PaginationMixin, ListView):
|
class OrderSearch(PaginationMixin, ListView):
|
||||||
model = Order
|
model = Order
|
||||||
|
paginator_class = LargeResultSetPaginator
|
||||||
context_object_name = 'orders'
|
context_object_name = 'orders'
|
||||||
template_name = 'pretixcontrol/search/orders.html'
|
template_name = 'pretixcontrol/search/orders.html'
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ class OrderSearch(PaginationMixin, ListView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = Order.objects.all().annotate(pcnt=Count('positions', distinct=True)).select_related('invoice_address')
|
qs = Order.objects.select_related('invoice_address')
|
||||||
if not self.request.user.is_superuser:
|
if not self.request.user.is_superuser:
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(event__organizer_id__in=self.request.user.teams.filter(
|
Q(event__organizer_id__in=self.request.user.teams.filter(
|
||||||
@@ -34,7 +35,7 @@ class OrderSearch(PaginationMixin, ListView):
|
|||||||
if self.filter_form.is_valid():
|
if self.filter_form.is_valid():
|
||||||
qs = self.filter_form.filter_qs(qs)
|
qs = self.filter_form.filter_qs(qs)
|
||||||
|
|
||||||
return qs.distinct().only(
|
return qs.only(
|
||||||
'id', 'invoice_address__name', 'code', 'event', 'email', 'datetime', 'total', 'status'
|
'id', 'invoice_address__name', 'code', 'event', 'email', 'datetime', 'total', 'status'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'event', 'event__organizer'
|
'event', 'event__organizer'
|
||||||
|
|||||||
Reference in New Issue
Block a user