Compare commits

...

3 Commits

Author SHA1 Message Date
Raphael Michel
d9d11883a1 Review notes 2025-02-21 15:39:29 +01:00
Raphael Michel
1acde9b8f9 Update src/pretix/base/models/log.py
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-02-11 15:40:42 +01:00
Raphael Michel
a9bf84b688 Improve efficiency of bulk operations 2025-02-11 10:56:37 +01:00
5 changed files with 86 additions and 69 deletions

View File

@@ -37,7 +37,7 @@ import logging
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import connections, models
from django.utils.functional import cached_property from django.utils.functional import cached_property
from pretix.base.logentrytype_registry import log_entry_types, make_link from pretix.base.logentrytype_registry import log_entry_types, make_link
@@ -165,6 +165,15 @@ class LogEntry(models.Model):
def delete(self, using=None, keep_parents=False): def delete(self, using=None, keep_parents=False):
raise TypeError("Logs cannot be deleted.") raise TypeError("Logs cannot be deleted.")
@classmethod
def bulk_create_and_postprocess(cls, objects):
if connections['default'].features.can_return_rows_from_bulk_insert:
cls.objects.bulk_create(objects)
else:
for le in objects:
le.save()
cls.bulk_postprocess(objects)
@classmethod @classmethod
def bulk_postprocess(cls, objects): def bulk_postprocess(cls, objects):
from pretix.api.webhooks import notify_webhooks from pretix.api.webhooks import notify_webhooks

View File

@@ -46,7 +46,7 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files import File from django.core.files import File
from django.db import connections, transaction from django.db import transaction
from django.db.models import ( from django.db.models import (
Count, Exists, F, IntegerField, Max, Min, OuterRef, Prefetch, Count, Exists, F, IntegerField, Max, Min, OuterRef, Prefetch,
ProtectedError, Q, Subquery, Sum, ProtectedError, Q, Subquery, Sum,
@@ -1159,13 +1159,7 @@ class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, Organizer
obj.log_action('pretix.device.changed', data=data, user=self.request.user, save=False) obj.log_action('pretix.device.changed', data=data, user=self.request.user, save=False)
) )
if connections['default'].features.can_return_rows_from_bulk_insert: LogEntry.bulk_create_and_postprocess(log_entries)
LogEntry.objects.bulk_create(log_entries, batch_size=200)
LogEntry.bulk_postprocess(log_entries)
else:
for le in log_entries:
le.save()
LogEntry.bulk_postprocess(log_entries)
messages.success(self.request, _('Your changes have been saved.')) messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form) return super().form_valid(form)

View File

@@ -40,7 +40,7 @@ from dateutil.rrule import rruleset
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files import File from django.core.files import File
from django.db import connections, transaction from django.db import transaction
from django.db.models import Count, F, Prefetch, ProtectedError from django.db.models import Count, F, Prefetch, ProtectedError
from django.db.models.functions import Coalesce, TruncDate, TruncTime from django.db.models.functions import Coalesce, TruncDate, TruncTime
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
@@ -657,24 +657,30 @@ class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View)
@transaction.atomic @transaction.atomic
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if request.POST.get('action') == 'disable': if request.POST.get('action') == 'disable':
log_entries = []
for obj in self.get_queryset(): for obj in self.get_queryset():
obj.log_action( log_entries.append(obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={ 'pretix.subevent.changed', user=self.request.user, data={
'active': False 'active': False
} }, save=False
) ))
obj.active = False obj.active = False
obj.save(update_fields=['active']) obj.save(update_fields=['active'])
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.')) messages.success(request, pgettext_lazy('subevent', 'The selected dates have been disabled.'))
elif request.POST.get('action') == 'enable': elif request.POST.get('action') == 'enable':
log_entries = []
for obj in self.get_queryset(): for obj in self.get_queryset():
obj.log_action( log_entries.append(obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={ 'pretix.subevent.changed', user=self.request.user, data={
'active': True 'active': True
} }, save=False
) ))
obj.active = True obj.active = True
obj.save(update_fields=['active']) obj.save(update_fields=['active'])
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been enabled.')) messages.success(request, pgettext_lazy('subevent', 'The selected dates have been enabled.'))
elif request.POST.get('action') == 'delete': elif request.POST.get('action') == 'delete':
return render(request, 'pretixcontrol/subevents/delete_bulk.html', { return render(request, 'pretixcontrol/subevents/delete_bulk.html', {
@@ -682,22 +688,28 @@ class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View)
'forbidden': self.get_queryset().filter(orderposition__isnull=False).distinct(), 'forbidden': self.get_queryset().filter(orderposition__isnull=False).distinct(),
}) })
elif request.POST.get('action') == 'delete_confirm': elif request.POST.get('action') == 'delete_confirm':
log_entries = []
to_delete = []
for obj in self.get_queryset(): for obj in self.get_queryset():
try: try:
if not obj.allow_delete(): if not obj.allow_delete():
raise ProtectedError('only deactivate', [obj]) raise ProtectedError('only deactivate', [obj])
CartPosition.objects.filter(addon_to__subevent=obj).delete() log_entries.append(obj.log_action('pretix.subevent.deleted', user=self.request.user, save=False))
obj.cartposition_set.all().delete() to_delete.append(obj.pk)
obj.log_action('pretix.subevent.deleted', user=self.request.user)
obj.delete()
except ProtectedError: except ProtectedError:
obj.log_action( log_entries.append(obj.log_action(
'pretix.subevent.changed', user=self.request.user, data={ 'pretix.subevent.changed', user=self.request.user, data={
'active': False 'active': False
} }, save=False,
) ))
obj.active = False obj.active = False
obj.save(update_fields=['active']) obj.save(update_fields=['active'])
if to_delete:
CartPosition.objects.filter(addon_to__subevent_id__in=to_delete).delete()
CartPosition.objects.filter(subevent_id__in=to_delete).delete()
SubEvent.objects.filter(pk__in=to_delete).delete()
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(request, pgettext_lazy('subevent', 'The selected dates have been deleted or disabled.')) messages.success(request, pgettext_lazy('subevent', 'The selected dates have been deleted or disabled.'))
return redirect(self.get_success_url()) return redirect(self.get_success_url())
@@ -1009,13 +1021,7 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
f.save() f.save()
set_progress(90) set_progress(90)
if connections['default'].features.can_return_rows_from_bulk_insert: LogEntry.bulk_create_and_postprocess(log_entries)
LogEntry.objects.bulk_create(log_entries)
LogEntry.bulk_postprocess(log_entries)
else:
for le in log_entries:
le.save()
LogEntry.bulk_postprocess(log_entries)
self.request.event.cache.clear() self.request.event.cache.clear()
return len(subevents) return len(subevents)
@@ -1578,13 +1584,7 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
self.save_itemvars() self.save_itemvars()
self.save_meta() self.save_meta()
if connections['default'].features.can_return_rows_from_bulk_insert: LogEntry.bulk_create_and_postprocess(log_entries)
LogEntry.objects.bulk_create(log_entries, batch_size=200)
LogEntry.bulk_postprocess(log_entries)
else:
for le in log_entries:
le.save()
LogEntry.bulk_postprocess(log_entries)
self.request.event.cache.clear() self.request.event.cache.clear()
messages.success(self.request, _('Your changes have been saved.')) messages.success(self.request, _('Your changes have been saved.'))

View File

@@ -477,7 +477,7 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
log_entries.append( log_entries.append(
v.log_action('pretix.voucher.added', data=data, user=self.request.user, save=False) v.log_action('pretix.voucher.added', data=data, user=self.request.user, save=False)
) )
LogEntry.objects.bulk_create(log_entries) LogEntry.bulk_create_and_postprocess(log_entries)
form.post_bulk_save(batch_vouchers) form.post_bulk_save(batch_vouchers)
batch_vouchers.clear() batch_vouchers.clear()
set_progress(len(voucherids) / total_num * (50. if form.cleaned_data['send'] else 100.)) set_progress(len(voucherids) / total_num * (50. if form.cleaned_data['send'] else 100.))
@@ -619,19 +619,26 @@ class VoucherBulkAction(EventPermissionRequiredMixin, View):
'forbidden': self.objects.exclude(redeemed=0), 'forbidden': self.objects.exclude(redeemed=0),
}) })
elif request.POST.get('action') == 'delete_confirm': elif request.POST.get('action') == 'delete_confirm':
log_entries = []
to_delete = []
for obj in self.objects: for obj in self.objects:
if obj.allow_delete(): if obj.allow_delete():
obj.log_action('pretix.voucher.deleted', user=self.request.user) log_entries.append(obj.log_action('pretix.voucher.deleted', user=self.request.user, save=False))
CartPosition.objects.filter(addon_to__voucher=obj).delete() to_delete.append(obj.pk)
obj.cartposition_set.all().delete()
obj.delete()
else: else:
obj.log_action('pretix.voucher.changed', user=self.request.user, data={ log_entries.append(obj.log_action('pretix.voucher.changed', user=self.request.user, data={
'max_usages': min(obj.redeemed, obj.max_usages), 'max_usages': min(obj.redeemed, obj.max_usages),
'bulk': True 'bulk': True
}) }), save=False)
obj.max_usages = min(obj.redeemed, obj.max_usages) obj.max_usages = min(obj.redeemed, obj.max_usages)
obj.save(update_fields=['max_usages']) obj.save(update_fields=['max_usages'])
if to_delete:
CartPosition.objects.filter(addon_to__voucher_id__in=to_delete).delete()
CartPosition.objects.filter(voucher_id__in=to_delete).delete()
Voucher.objects.filter(pk__in=to_delete).delete()
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(request, _('The selected vouchers have been deleted or disabled.')) messages.success(request, _('The selected vouchers have been deleted or disabled.'))
return redirect(self.get_success_url()) return redirect(self.get_success_url())

View File

@@ -49,7 +49,7 @@ from django.utils.translation import gettext_lazy as _, pgettext
from django.views import View from django.views import View
from django.views.generic import ListView from django.views.generic import ListView
from pretix.base.models import Item, Quota, WaitingListEntry from pretix.base.models import Item, LogEntry, Quota, WaitingListEntry
from pretix.base.models.waitinglist import WaitingListException from pretix.base.models.waitinglist import WaitingListException
from pretix.base.services.waitinglist import assign_automatically from pretix.base.services.waitinglist import assign_automatically
from pretix.base.views.tasks import AsyncAction from pretix.base.views.tasks import AsyncAction
@@ -160,10 +160,15 @@ class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMix
'forbidden': self.get_queryset().filter(voucher__isnull=False), 'forbidden': self.get_queryset().filter(voucher__isnull=False),
}) })
elif request.POST.get('action') == 'delete_confirm': elif request.POST.get('action') == 'delete_confirm':
for obj in self.get_queryset(force_filtered=True): with transaction.atomic():
if not obj.voucher_id: log_entries = []
obj.log_action('pretix.event.orders.waitinglist.deleted', user=self.request.user) to_delete = []
obj.delete() for obj in self.get_queryset(force_filtered=True):
if not obj.voucher_id:
log_entries.append(obj.log_action('pretix.event.orders.waitinglist.deleted', user=self.request.user, save=False))
to_delete.append(obj.pk)
WaitingListEntry.objects.filter(id__in=to_delete).delete()
LogEntry.bulk_create_and_postprocess(log_entries)
messages.success(request, _('The selected entries have been deleted.')) messages.success(request, _('The selected entries have been deleted.'))
return self._redirect_back() return self._redirect_back()
@@ -186,16 +191,17 @@ class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMix
if 'move_top' in request.POST: if 'move_top' in request.POST:
try: try:
wle = WaitingListEntry.objects.get( with transaction.atomic():
pk=request.POST.get('move_top'), event=self.request.event, wle = WaitingListEntry.objects.get(
) pk=request.POST.get('move_top'), event=self.request.event,
wle.priority = self.request.event.waitinglistentries.aggregate(m=Max('priority'))['m'] + 1 )
wle.save(update_fields=['priority']) wle.priority = self.request.event.waitinglistentries.aggregate(m=Max('priority'))['m'] + 1
wle.log_action( wle.save(update_fields=['priority'])
'pretix.event.orders.waitinglist.changed', wle.log_action(
data={'priority': wle.priority}, 'pretix.event.orders.waitinglist.changed',
user=self.request.user, data={'priority': wle.priority},
) user=self.request.user,
)
messages.success(request, _('The waiting list entry has been moved to the top.')) messages.success(request, _('The waiting list entry has been moved to the top.'))
return self._redirect_back() return self._redirect_back()
except WaitingListEntry.DoesNotExist: except WaitingListEntry.DoesNotExist:
@@ -204,16 +210,17 @@ class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMix
if 'move_end' in request.POST: if 'move_end' in request.POST:
try: try:
wle = WaitingListEntry.objects.get( with transaction.atomic():
pk=request.POST.get('move_end'), event=self.request.event, wle = WaitingListEntry.objects.get(
) pk=request.POST.get('move_end'), event=self.request.event,
wle.priority = self.request.event.waitinglistentries.aggregate(m=Min('priority'))['m'] - 1 )
wle.save(update_fields=['priority']) wle.priority = self.request.event.waitinglistentries.aggregate(m=Min('priority'))['m'] - 1
wle.log_action( wle.save(update_fields=['priority'])
'pretix.event.orders.waitinglist.changed', wle.log_action(
data={'priority': wle.priority}, 'pretix.event.orders.waitinglist.changed',
user=self.request.user, data={'priority': wle.priority},
) user=self.request.user,
)
messages.success(request, _('The waiting list entry has been moved to the end of the list.')) messages.success(request, _('The waiting list entry has been moved to the end of the list.'))
return self._redirect_back() return self._redirect_back()
except WaitingListEntry.DoesNotExist: except WaitingListEntry.DoesNotExist: