Compare commits

..

4 Commits

Author SHA1 Message Date
Raphael Michel
cedd0c877e Reduce search to attendee names 2021-03-25 20:01:29 +01:00
Raphael Michel
c9cebd820a Safeguard against infinite quotas 2021-03-24 09:03:27 +01:00
Raphael Michel
e2a40aaa3a Do not update query cache at all 2021-03-23 20:34:48 +01:00
Raphael Michel
584ff16f58 Add a quota cache in redis 2021-03-23 19:53:32 +01:00
14 changed files with 74 additions and 106 deletions

View File

@@ -1 +1 @@
__version__ = "3.17.0.dev0"
__version__ = "3.16.0"

View File

@@ -596,7 +596,6 @@ class EventSettingsSerializer(SettingsSerializer):
'checkout_email_helptext',
'presale_has_ended_text',
'voucher_explanation_text',
'checkout_success_text',
'banner_text',
'banner_text_bottom',
'show_dates_on_frontpage',
@@ -657,7 +656,6 @@ class EventSettingsSerializer(SettingsSerializer):
'mail_from',
'mail_from_name',
'mail_attach_ical',
'mail_attach_tickets',
'invoice_address_asked',
'invoice_address_required',
'invoice_address_vatid',

View File

@@ -736,14 +736,7 @@ with scopes_disabled():
def search_qs(self, queryset, name, value):
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name_cached__icontains=value)
| Q(addon_to__attendee_name_cached__icontains=value)
| Q(attendee_email__icontains=value)
| Q(addon_to__attendee_email__icontains=value)
| Q(order__code__istartswith=value)
| Q(order__invoice_address__name_cached__icontains=value)
| Q(order__email__icontains=value)
Q(attendee_name_cached__icontains=value)
)
def has_checkin_qs(self, queryset, name, value):

View File

@@ -13,7 +13,6 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _, pgettext
from pretix.base.models import Invoice, InvoiceLine, OrderPayment
from ..services.export import ExportError
from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat
@@ -112,8 +111,6 @@ class InvoiceExporter(InvoiceExporterMixin, BaseExporter):
if not i.file:
invoice_pdf_task.apply(args=(i.pk,))
i.refresh_from_db()
if not i.file:
raise ExportError('Could not generate PDF for invoice {nr}'.format(nr=i.full_invoice_no))
i.file.open('rb')
zipf.writestr('{}-{}.pdf'.format(i.number, i.order.code), i.file.read())
i.file.close()

View File

@@ -255,10 +255,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
invoice_from_top = 17 * mm
def _draw_invoice_from(self, canvas):
p = Paragraph(
bleach.clean(self.invoice.full_invoice_from, tags=[]).strip().replace('\n', '<br />\n'),
style=self.stylesheet['InvoiceFrom']
)
p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '<br />\n'), style=self.stylesheet[
'InvoiceFrom'])
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
p_size = p.wrap(self.invoice_from_width, self.invoice_from_height)
p.drawOn(canvas, self.invoice_from_left, self.pagesize[1] - p_size[1] - self.invoice_from_top)
@@ -363,7 +361,6 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
def _draw_event(self, canvas):
def shorten(txt):
txt = str(txt)
txt = bleach.clean(txt, tags=[]).strip()
p = Paragraph(txt.strip().replace('\n', '<br />\n'), style=self.stylesheet['Normal'])
p_size = p.wrap(self.event_width, self.event_height)
@@ -444,18 +441,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story = []
if self.invoice.custom_field:
story.append(Paragraph(
'{}: {}'.format(
bleach.clean(self.invoice.event.settings.invoice_address_custom_field, tags=[]).strip().replace('\n', '<br />\n'),
bleach.clean(self.invoice.custom_field, tags=[]).strip().replace('\n', '<br />\n'),
),
'{}: {}'.format(self.invoice.event.settings.invoice_address_custom_field, self.invoice.custom_field),
self.stylesheet['Normal']
))
if self.invoice.internal_reference:
story.append(Paragraph(
pgettext('invoice', 'Customer reference: {reference}').format(
reference=bleach.clean(self.invoice.internal_reference, tags=[]).strip().replace('\n', '<br />\n'),
),
pgettext('invoice', 'Customer reference: {reference}').format(reference=self.invoice.internal_reference),
self.stylesheet['Normal']
))
@@ -474,10 +466,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
))
if self.invoice.introductory_text:
story.append(Paragraph(
bleach.clean(self.invoice.introductory_text, tags=[]).strip().replace('\n', '<br />\n'),
self.stylesheet['Normal']
))
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
story.append(Spacer(1, 10 * mm))
return story
@@ -577,16 +566,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
story.append(Spacer(1, 15 * mm))
if self.invoice.payment_provider_text:
story.append(Paragraph(
bleach.clean(self.invoice.payment_provider_text, tags=[]).strip().replace('\n', '<br />\n'),
self.stylesheet['Normal']
))
story.append(Paragraph(self.invoice.payment_provider_text, self.stylesheet['Normal']))
if self.invoice.additional_text:
story.append(Paragraph(
bleach.clean(self.invoice.additional_text, tags=[]).strip().replace('\n', '<br />\n'),
self.stylesheet['Normal']
))
story.append(Paragraph(self.invoice.additional_text, self.stylesheet['Normal']))
story.append(Spacer(1, 15 * mm))
tstyledata = [
@@ -718,10 +701,7 @@ class Modern1Renderer(ClassicInvoiceRenderer):
def _draw_invoice_from(self, canvas):
if not self.invoice.invoice_from:
return
c = [
bleach.clean(l, tags=[]).strip().replace('\n', '<br />\n')
for l in self.invoice.address_invoice_from.strip().split('\n')
]
c = self.invoice.address_invoice_from.strip().split('\n')
p = Paragraph(' · '.join(c), style=self.stylesheet['Sender'])
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)

View File

@@ -291,8 +291,6 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
order = None
else:
with language(order.locale, event.settings.region):
if not event.settings.mail_attach_tickets:
attach_tickets = False
if position:
try:
position = order.positions.get(pk=position)

View File

@@ -10,6 +10,7 @@ from django.db.models import (
)
from django.dispatch import receiver
from django.utils.timezone import now
from django_redis import get_redis_connection
from django_scopes import scopes_disabled
from pretix.base.models import (
@@ -89,7 +90,7 @@ class QuotaAvailability:
def queue(self, *quota):
self._queue += quota
def compute(self, now_dt=None):
def compute(self, now_dt=None, dbcache=True):
now_dt = now_dt or now()
quotas = list(set(self._queue))
quotas_original = list(self._queue)
@@ -106,18 +107,18 @@ class QuotaAvailability:
self._close(quotas)
try:
self._write_cache(quotas, now_dt)
self._write_cache(quotas, now_dt, dbcache)
except OperationalError as e:
# Ignore deadlocks when multiple threads try to write to the cache
if 'deadlock' not in str(e).lower():
raise e
def _write_cache(self, quotas, now_dt):
def _write_cache(self, quotas, now_dt, dbcache):
# We used to also delete item_quota_cache:* from the event cache here, but as the cache
# gets more complex, this does not seem worth it. The cache is only present for up to
# 5 seconds to prevent high peaks, and a 5-second delay in availability is usually
# tolerable
update = []
update = defaultdict(list)
for q in quotas:
rewrite_cache = self._count_waitinglist and (
not q.cache_is_hot(now_dt) or self.results[q][0] > q.cached_availability_state
@@ -129,12 +130,21 @@ class QuotaAvailability:
q.cached_availability_time = now_dt
if q in self.count_paid_orders:
q.cached_availability_paid_orders = self.count_paid_orders[q]
update.append(q)
update[q.event_id].append(q)
if update:
Quota.objects.using('default').bulk_update(update, [
'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
'cached_availability_paid_orders'
], batch_size=50)
# if dbcache:
# Quota.objects.using('default').bulk_update(sum((quotas for event, quotas in update.items()), []), [
# 'cached_availability_state', 'cached_availability_number', 'cached_availability_time',
# 'cached_availability_paid_orders'
# ], batch_size=50)
if settings.HAS_REDIS:
rc = get_redis_connection("redis")
for eventid, quotas in update.items():
rc.hmset(f'quotas:{eventid}:availability', {
str(q.id): ",".join([str(i) for i in self.results[q]]) for q in quotas
})
def _close(self, quotas):
for q in quotas:

View File

@@ -13,7 +13,6 @@ from django.core.validators import (
MaxValueValidator, MinValueValidator, RegexValidator,
)
from django.db.models import Model
from django.utils.text import format_lazy
from django.utils.translation import (
gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy,
)
@@ -1323,19 +1322,6 @@ DEFAULTS = {
'default': 'classic',
'type': str
},
'mail_attach_tickets': {
'default': 'True',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Attach ticket files"),
help_text=format_lazy(
_("Tickets will never be attached if they're larger than {size} to avoid email delivery problems."),
size='4 MB'
),
)
},
'mail_attach_ical': {
'default': 'False',
'type': bool,
@@ -1997,19 +1983,6 @@ Your {event} team"""))
"why you need information from them.")
)
},
'checkout_success_text': {
'default': '',
'type': LazyI18nString,
'serializer_class': I18nField,
'form_class': I18nFormField,
'form_kwargs': dict(
label=_("Additional success message"),
help_text=_("This message will be shown after an order has been created successfully. It will be shown in additional "
"to the default text."),
widget_kwargs={'attrs': {'rows': '2'}},
widget=I18nTextarea
)
},
'checkout_phone_helptext': {
'default': '',
'type': LazyI18nString,

View File

@@ -435,7 +435,6 @@ class EventSettingsForm(SettingsForm):
'checkout_email_helptext',
'presale_has_ended_text',
'voucher_explanation_text',
'checkout_success_text',
'show_dates_on_frontpage',
'show_date_to',
'show_times',
@@ -787,7 +786,6 @@ class MailSettingsForm(SettingsForm):
'mail_from',
'mail_from_name',
'mail_attach_ical',
'mail_attach_tickets',
]
mail_sales_channel_placed_paid = forms.MultipleChoiceField(

View File

@@ -16,7 +16,6 @@
{% bootstrap_field form.mail_from_name layout="control" %}
{% bootstrap_field form.mail_text_signature layout="control" %}
{% bootstrap_field form.mail_bcc layout="control" %}
{% bootstrap_field form.mail_attach_tickets layout="control" %}
{% bootstrap_field form.mail_attach_ical layout="control" %}
{% bootstrap_field form.mail_sales_channel_placed_paid layout="control" %}
</fieldset>

View File

@@ -188,7 +188,6 @@
</div>
</div>
{% bootstrap_field sform.checkout_success_text layout="control" %}
{% bootstrap_field sform.checkout_email_helptext layout="control" %}
{% bootstrap_field sform.checkout_phone_helptext layout="control" %}
{% bootstrap_field sform.banner_text layout="control" %}

View File

@@ -6,7 +6,6 @@
{% load expiresformat %}
{% load eventurl %}
{% load phone_format %}
{% load rich_text %}
{% block title %}
{% if "thanks" in request.GET or "paid" in request.GET %}
{% trans "Thank you!" %}
@@ -45,9 +44,6 @@
{% else %}
<p>{% trans "We successfully received your payment. See below for details." %}</p>
{% endif %}
{% if request.event.settings.checkout_success_text %}
{{ request.event.settings.checkout_success_text|rich_text }}
{% endif %}
<p class="iframe-hidden">{% blocktrans trimmed %}
Please bookmark or save the link to this exact page if you want to access your order later. We also sent you an email containing the link to the address you specified.
{% endblocktrans %}</p>

View File

@@ -14,6 +14,7 @@ from django.utils.timezone import get_current_timezone, now
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.generic import ListView, TemplateView
from django_redis import get_redis_connection
from pytz import UTC
from pretix.base.i18n import language
@@ -366,35 +367,61 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_n
'date_from'
)
quotas_to_compute = []
quotas_by_event = defaultdict(list)
for se in qs:
if se.presale_is_running:
quotas_to_compute += [
q for q in se.active_quotas
if not q.cache_is_hot(now() + timedelta(seconds=5))
]
quotas_by_event[se.event_id] += [
q for q in se.active_quotas
if not q.cache_is_hot(now() + timedelta(seconds=5))
]
name = None
qcache = {}
if quotas_to_compute:
qa = QuotaAvailability()
qa.queue(*quotas_to_compute)
qa.compute()
for se in qs:
if settings.HAS_REDIS:
rc = get_redis_connection("redis")
for ev, quotas in quotas_by_event.items():
d = rc.hmget(f'quotas:{ev}:availability', [str(q.pk) for q in quotas])
for redisval, q in zip(d, quotas):
if redisval is not None and b',' in redisval:
parts = redisval.decode().strip().split(',')
if parts[0].isdigit() and parts[1] == "None":
qcache[q] = (int(parts[0]), None)
quotas_to_compute.remove(q)
else:
try:
qcache[q] = tuple(int(rv) for rv in parts)
quotas_to_compute.remove(q)
except ValueError:
pass
if quotas_to_compute:
se._quota_cache = qa.results
qa = QuotaAvailability()
qa.queue(*quotas_to_compute)
qa.compute(dbcache=False)
qcache.update(qa.results)
for se in qs:
if qcache:
se._quota_cache = qcache
kwargs = {'subevent': se.pk}
if cart_namespace:
kwargs['cart_namespace'] = cart_namespace
settings = event.settings if event else se.event.settings
timezones.add(settings.timezones)
tz = pytz.timezone(settings.timezone)
s = event.settings if event else se.event.settings
timezones.add(s.timezones)
tz = pytz.timezone(s.timezone)
datetime_from = se.date_from.astimezone(tz)
date_from = datetime_from.date()
if name is None:
name = str(se.name)
elif str(se.name) != name:
ebd['_subevents_different_names'] = True
if se.event.settings.show_date_to and se.date_to:
if s.show_date_to and se.date_to:
datetime_to = se.date_to.astimezone(tz)
date_to = se.date_to.astimezone(tz).date()
d = max(date_from, before.date())
@@ -402,13 +429,13 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_n
first = d == date_from
ebd[d].append({
'continued': not first,
'timezone': settings.timezone,
'time': datetime_from.time().replace(tzinfo=None) if first and settings.show_times else None,
'timezone': s.timezone,
'time': datetime_from.time().replace(tzinfo=None) if first and s.show_times else None,
'time_end': (
datetime_to.time().replace(tzinfo=None)
if (date_to == date_from or (
date_to == date_from + timedelta(days=1) and datetime_to.time() < datetime_from.time()
)) and settings.show_times
)) and s.show_times
else None
),
'event': se,
@@ -420,9 +447,9 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, event=None, cart_n
ebd[date_from].append({
'event': se,
'continued': False,
'time': datetime_from.time().replace(tzinfo=None) if se.event.settings.show_times else None,
'time': datetime_from.time().replace(tzinfo=None) if s.show_times else None,
'url': eventreverse(se.event, 'presale:event.index', kwargs=kwargs),
'timezone': se.event.settings.timezone,
'timezone': s.timezone,
})

View File

@@ -918,10 +918,12 @@ class SubEventsTest(SoupTest):
assert doc.select(".alert-success")
with scopes_disabled():
for se in [self.subevent1, self.subevent2]:
q = se.quotas.get(name='Q1')
q = se.quotas.first()
assert q.name == 'Q1'
assert q.size == 50
assert list(q.items.all()) == [self.ticket]
q = se.quotas.get(name='Q2')
q = se.quotas.last()
assert q.name == 'Q2'
assert q.size == 25
assert list(q.items.all()) == [self.ticket]
@@ -929,8 +931,6 @@ class SubEventsTest(SoupTest):
'__ALL': 'on',
}, follow=True)
fields = extract_form_fields(doc)
assert fields['quotas-0-name'] == 'Q1'
assert fields['quotas-1-name'] == 'Q2'
fields.update({
'_bulk': ['__quotas'],
'quotas-0-size': '25',