forked from CGM_Public/pretix_original
Event cancellation: Add safety and security checks (#5565)
* Event cancellation: Add safety and security checks When cancelling an event, a large sum of money might be refunded instantly. This PR adds safety features around this by - doing a dry-run first that shows a preview of the expected refund sum - sending a confirmation mode via email for any automatic refunds of more than 100 currency units - keeping a more detailed log of the settings this was executed with * Update src/pretix/control/views/orders.py Co-authored-by: luelista <weller@rami.io> --------- Co-authored-by: luelista <weller@rami.io>
This commit is contained in:
@@ -42,6 +42,7 @@ from datetime import datetime, time, timedelta
|
||||
from decimal import Decimal, DecimalException
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -122,8 +123,8 @@ from pretix.control.forms.filter import (
|
||||
RefundFilterForm,
|
||||
)
|
||||
from pretix.control.forms.orders import (
|
||||
CancelForm, CommentForm, DenyForm, EventCancelForm, ExporterForm,
|
||||
ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeAddForm,
|
||||
CancelForm, CommentForm, DenyForm, EventCancelConfirmForm, EventCancelForm,
|
||||
ExporterForm, ExtendForm, MarkPaidForm, OrderContactForm, OrderFeeAddForm,
|
||||
OrderFeeAddFormset, OrderFeeChangeForm, OrderLocaleForm, OrderMailForm,
|
||||
OrderPositionAddForm, OrderPositionAddFormset, OrderPositionChangeForm,
|
||||
OrderPositionMailForm, OrderRefundForm, OtherOperationsForm,
|
||||
@@ -2975,10 +2976,99 @@ class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView):
|
||||
send_waitinglist_subject=form.cleaned_data.get('send_waitinglist_subject').data,
|
||||
send_waitinglist_message=form.cleaned_data.get('send_waitinglist_message').data,
|
||||
user=self.request.user.pk,
|
||||
dry_run=settings.HAS_CELERY,
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(
|
||||
dry_run_supported=settings.HAS_CELERY,
|
||||
)
|
||||
|
||||
def get_success_message(self, value):
|
||||
if value == 0:
|
||||
if value["dry_run"]:
|
||||
return None
|
||||
elif value["failed"] == 0:
|
||||
return _('All orders have been canceled.')
|
||||
else:
|
||||
return _('The orders have been canceled. An error occurred with {count} orders, please '
|
||||
'check all uncanceled orders.').format(count=value)
|
||||
|
||||
def get_success_url(self, value):
|
||||
if settings.HAS_CELERY:
|
||||
return reverse('control:event.cancel.confirm', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'task': value["id"],
|
||||
})
|
||||
else:
|
||||
return reverse('control:event.cancel', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
def get_error_url(self):
|
||||
return reverse('control:event.cancel', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
def get_error_message(self, exception):
|
||||
if isinstance(exception, str):
|
||||
return exception
|
||||
return super().get_error_message(exception)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your input was not valid.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class EventCancelConfirm(EventPermissionRequiredMixin, AsyncAction, FormView):
|
||||
template_name = 'pretixcontrol/orders/cancel_confirm.html'
|
||||
permission = 'can_change_orders'
|
||||
form_class = EventCancelConfirmForm
|
||||
task = cancel_event
|
||||
known_errortypes = ['OrderError']
|
||||
|
||||
@cached_property
|
||||
def dryrun_result(self):
|
||||
res = AsyncResult(self.kwargs.get("task"))
|
||||
if not res.ready():
|
||||
raise Http404()
|
||||
if not res.successful():
|
||||
raise Http404()
|
||||
data = res.info
|
||||
if not data.get("dry_run"):
|
||||
raise Http404()
|
||||
if data.get("args")[0] != self.request.event.pk:
|
||||
raise Http404()
|
||||
return data
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'async_id' in request.GET and settings.HAS_CELERY:
|
||||
return self.get_result(request)
|
||||
return FormView.get(self, request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
k = super().get_form_kwargs()
|
||||
k['confirmation_code'] = self.dryrun_result["confirmation_code"]
|
||||
return k
|
||||
|
||||
def form_valid(self, form):
|
||||
return self.do(
|
||||
*self.dryrun_result["args"],
|
||||
**{
|
||||
**self.dryrun_result["kwargs"],
|
||||
"dry_run": False,
|
||||
},
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(
|
||||
dryrun_result=self.dryrun_result,
|
||||
)
|
||||
|
||||
def get_success_message(self, value):
|
||||
if value["failed"] == 0:
|
||||
return _('All orders have been canceled.')
|
||||
else:
|
||||
return _('The orders have been canceled. An error occurred with {count} orders, please '
|
||||
|
||||
Reference in New Issue
Block a user