mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Add bulk operations for orders (#3548)
* Add bulk operations for orders * UI tweaks * Fix test failures * Fix filter form * Add tests * Run isort
This commit is contained in:
@@ -192,7 +192,6 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
|
||||
|
||||
|
||||
class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncPostView):
|
||||
template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
|
||||
permission = ('can_change_orders', 'can_checkin_orders')
|
||||
context_object_name = 'device'
|
||||
|
||||
|
||||
@@ -45,12 +45,12 @@ from urllib.parse import quote, urlencode
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, IntegerField, OuterRef, Prefetch, ProtectedError, Q,
|
||||
Subquery, Sum,
|
||||
QuerySet, Subquery, Sum,
|
||||
)
|
||||
from django.forms import formset_factory
|
||||
from django.http import (
|
||||
@@ -111,16 +111,16 @@ from pretix.base.signals import (
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.base.views.mixins import OrderQuestionsViewMixin
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.base.views.tasks import AsyncAction, AsyncFormView
|
||||
from pretix.control.forms.exports import ScheduledEventExportForm
|
||||
from pretix.control.forms.filter import (
|
||||
EventOrderExpertFilterForm, EventOrderFilterForm, OverviewFilterForm,
|
||||
RefundFilterForm,
|
||||
)
|
||||
from pretix.control.forms.orders import (
|
||||
CancelForm, CommentForm, ConfirmPaymentForm, EventCancelForm, ExporterForm,
|
||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeChangeForm,
|
||||
OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||
CancelForm, CommentForm, ConfirmPaymentForm, DenyForm, EventCancelForm,
|
||||
ExporterForm, ExtendForm, MarkPaidForm, OrderContactForm,
|
||||
OrderFeeChangeForm, OrderLocaleForm, OrderMailForm, OrderPositionAddForm,
|
||||
OrderPositionAddFormset, OrderPositionChangeForm, OrderPositionMailForm,
|
||||
OrderRefundForm, OtherOperationsForm, ReactivateOrderForm,
|
||||
)
|
||||
@@ -137,10 +137,17 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrderSearchMixin:
|
||||
|
||||
@cached_property
|
||||
def request_data(self):
|
||||
if self.request.method == "POST":
|
||||
return self.request.POST
|
||||
return self.request.GET
|
||||
|
||||
def get_forms(self):
|
||||
f = [
|
||||
EventOrderExpertFilterForm(
|
||||
data=self.request.GET,
|
||||
data=self.request_data,
|
||||
event=self.request.event,
|
||||
prefix='expert',
|
||||
)
|
||||
@@ -160,6 +167,167 @@ class OrderSearch(OrderSearchMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
return ctx
|
||||
|
||||
|
||||
class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, AsyncFormView):
|
||||
template_name = 'pretixcontrol/orders/bulk_action.html'
|
||||
permission = 'can_change_orders'
|
||||
form_class = forms.Form
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Order.objects.filter(
|
||||
event=self.request.event
|
||||
).select_related('invoice_address')
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
for f in self.get_forms():
|
||||
if any(k.startswith(f.prefix) for k in self.request.POST.keys()):
|
||||
if not f.is_valid():
|
||||
raise PermissionDenied("Invalid query") # better safe than sorry with this one
|
||||
qs = f.filter_qs(qs)
|
||||
|
||||
if 'order' in self.request_data and '__ALL' not in self.request_data:
|
||||
qs = qs.filter(
|
||||
id__in=self.request_data.getlist('order')
|
||||
)
|
||||
elif '__ALL' not in self.request_data:
|
||||
raise PermissionDenied("Invalid query") # better safe than sorry with this one
|
||||
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return EventOrderFilterForm(data=self.request.POST, event=self.request.event)
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
def allowed_for(self, queryset: QuerySet) -> QuerySet:
|
||||
raise NotImplementedError()
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
raise NotImplementedError()
|
||||
|
||||
def execute_bulk(self, queryset: QuerySet, form: forms.Form):
|
||||
qs = self.allowed_for(self.allowed_for(self.get_queryset()))
|
||||
total = qs.count()
|
||||
for i, o in enumerate(qs):
|
||||
self.execute_single(o, form)
|
||||
if i % 100 == 0:
|
||||
self.async_set_progress(i / total * 100)
|
||||
|
||||
def get_error_url(self):
|
||||
return self.get_success_url(None)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
return render(request, self.template_name, self.get_context_data())
|
||||
|
||||
def get_success_url(self, value):
|
||||
return reverse('control:event.orders', kwargs={
|
||||
'event': self.request.event.slug,
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['total'] = self.get_queryset().count()
|
||||
ctx['allowed'] = self.allowed_for(self.get_queryset())
|
||||
ctx['label'] = self.label
|
||||
ctx['form'] = self.get_form()
|
||||
return ctx
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = {
|
||||
"initial": self.get_initial(),
|
||||
"prefix": self.get_prefix(),
|
||||
}
|
||||
|
||||
if self.request.method in ("POST", "PUT") and self.request.POST.get("operation") == "confirm":
|
||||
kwargs.update(
|
||||
{
|
||||
"data": self.request.POST,
|
||||
"files": self.request.FILES,
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handle POST requests: instantiate a form instance with the passed
|
||||
POST variables and then check if it's valid.
|
||||
"""
|
||||
form = self.get_form()
|
||||
if self.request.POST.get("operation") == "confirm" and form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_prefix(self):
|
||||
return "bulkactionform"
|
||||
|
||||
@transaction.atomic()
|
||||
def async_form_valid(self, task, form):
|
||||
self.execute_bulk(self.allowed_for(self.get_queryset()), form)
|
||||
|
||||
|
||||
class OrderApproveBulkActionView(BaseOrderBulkActionView):
|
||||
label = _("Approve")
|
||||
|
||||
def allowed_for(self, queryset):
|
||||
return queryset.filter(
|
||||
status=Order.STATUS_PENDING,
|
||||
require_approval=True,
|
||||
)
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
approve_order(instance, user=self.request.user)
|
||||
|
||||
|
||||
class OrderDenyBulkActionView(BaseOrderBulkActionView):
|
||||
label = _("Deny")
|
||||
form_class = DenyForm
|
||||
|
||||
def allowed_for(self, queryset):
|
||||
return queryset.filter(
|
||||
status=Order.STATUS_PENDING,
|
||||
require_approval=True,
|
||||
)
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
deny_order(instance, user=self.request.user,
|
||||
comment=form.cleaned_data.get('comment') or None,
|
||||
send_mail=form.cleaned_data['send_email'])
|
||||
|
||||
|
||||
class OrderExpireBulkActionView(BaseOrderBulkActionView):
|
||||
label = _("Mark as expired if overdue")
|
||||
|
||||
def allowed_for(self, queryset):
|
||||
return queryset.filter(
|
||||
status=Order.STATUS_PENDING,
|
||||
require_approval=False,
|
||||
expires__lt=now(),
|
||||
)
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
mark_order_expired(instance, user=self.request.user)
|
||||
|
||||
|
||||
class OrderDeleteBulkActionView(BaseOrderBulkActionView):
|
||||
label = _("Delete")
|
||||
|
||||
def allowed_for(self, queryset):
|
||||
return queryset.filter(
|
||||
testmode=True,
|
||||
)
|
||||
|
||||
def execute_single(self, instance, form: forms.Form):
|
||||
instance.gracefully_delete(user=self.request.user)
|
||||
|
||||
|
||||
class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
model = Order
|
||||
context_object_name = 'orders'
|
||||
@@ -183,6 +351,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['filter_form'] = self.filter_form
|
||||
ctx['filter_forms'] = self.get_forms()
|
||||
|
||||
ctx['filter_strings'] = []
|
||||
for f in self.get_forms():
|
||||
@@ -607,21 +776,26 @@ class OrderDelete(OrderView):
|
||||
class OrderDeny(OrderView):
|
||||
permission = 'can_change_orders'
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
def post(self, request, *args, **kwargs):
|
||||
if self.order.require_approval:
|
||||
try:
|
||||
deny_order(self.order, user=self.request.user,
|
||||
comment=self.request.POST.get('comment'),
|
||||
send_mail=self.request.POST.get('send_email') == 'on')
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
form = DenyForm(self.request.POST if self.request.method == "POST" else None)
|
||||
if form.is_valid():
|
||||
try:
|
||||
deny_order(self.order, user=self.request.user,
|
||||
comment=self.request.POST.get('comment'),
|
||||
send_mail=self.request.POST.get('send_email') == 'on')
|
||||
except OrderError as e:
|
||||
messages.error(self.request, str(e))
|
||||
else:
|
||||
messages.success(self.request, _('The order has been denied and is therefore now canceled.'))
|
||||
else:
|
||||
messages.success(self.request, _('The order has been denied and is therefore now canceled.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return render(self.request, 'pretixcontrol/order/deny.html', {
|
||||
'order': self.order,
|
||||
'form': DenyForm(self.request.POST if self.request.method == "POST" else None)
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user