diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py
index 3498f13f2..099d13a8b 100644
--- a/src/pretix/base/models/event.py
+++ b/src/pretix/base/models/event.py
@@ -282,10 +282,10 @@ class Event(EventMixin, LoggedModel):
if not really:
raise TypeError("Pass really=True as a parameter.")
- OrderPosition.objects.all().delete(order__event=self)
- OrderFee.objects.all().delete(order__event=self)
- OrderPayment.objects.all().delete(order__event=self)
- OrderRefund.objects.all().delete(order__event=self)
+ OrderPosition.objects.filter(order__event=self).delete()
+ OrderFee.objects.filter(order__event=self).delete()
+ OrderPayment.objects.filter(order__event=self).delete()
+ OrderRefund.objects.filter(order__event=self).delete()
self.orders.all().delete()
def save(self, *args, **kwargs):
@@ -301,7 +301,7 @@ class Event(EventMixin, LoggedModel):
return []
return self.plugins.split(",")
- def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
+ def get_cache(self):
"""
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
Django's built-in cache backends, but puts you into an isolated environment for
diff --git a/src/pretix/base/models/organizer.py b/src/pretix/base/models/organizer.py
index a8b5e23e7..af4e60a05 100644
--- a/src/pretix/base/models/organizer.py
+++ b/src/pretix/base/models/organizer.py
@@ -82,6 +82,20 @@ class Organizer(LoggedModel):
return ObjectRelatedCache(self)
+ def allow_delete(self):
+ from . import Order, Invoice
+ return (
+ not Order.objects.filter(event__organizer=self).exists() and
+ not Invoice.objects.filter(event__organizer=self).exists() and
+ not self.devices.exists()
+ )
+
+ def delete_sub_objects(self):
+ for e in self.events.all():
+ e.delete_sub_objects()
+ e.delete()
+ self.teams.all().delete()
+
def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)
diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py
index cee791c72..6a8a8b146 100644
--- a/src/pretix/control/forms/organizer.py
+++ b/src/pretix/control/forms/organizer.py
@@ -31,6 +31,29 @@ class OrganizerForm(I18nModelForm):
return slug
+class OrganizerDeleteForm(forms.Form):
+ error_messages = {
+ 'slug_wrong': _("The slug you entered was not correct."),
+ }
+ slug = forms.CharField(
+ max_length=255,
+ label=_("Event slug"),
+ )
+
+ def __init__(self, *args, **kwargs):
+ self.organizer = kwargs.pop('organizer')
+ super().__init__(*args, **kwargs)
+
+ def clean_slug(self):
+ slug = self.cleaned_data.get('slug')
+ if slug != self.organizer.slug:
+ raise forms.ValidationError(
+ self.error_messages['slug_wrong'],
+ code='slug_wrong',
+ )
+ return slug
+
+
class OrganizerUpdateForm(OrganizerForm):
def __init__(self, *args, **kwargs):
diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py
index b1d58f276..c006883bf 100644
--- a/src/pretix/control/logdisplay.py
+++ b/src/pretix/control/logdisplay.py
@@ -215,6 +215,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
'pretix.user.settings.notifications.enabled': _('Notifications have been enabled.'),
'pretix.user.settings.notifications.disabled': _('Notifications have been disabled.'),
'pretix.user.settings.notifications.changed': _('Your notification settings have been changed.'),
+ 'pretix.organizer.deleted': _('The organizer "{name}" has been deleted.'),
'pretix.user.oauth.authorized': _('The application "{application_name}" has been authorized to access your '
'account.'),
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/base.html b/src/pretix/control/templates/pretixcontrol/organizers/base.html
index 7a67d8f70..e4dfc1fb2 100644
--- a/src/pretix/control/templates/pretixcontrol/organizers/base.html
+++ b/src/pretix/control/templates/pretixcontrol/organizers/base.html
@@ -12,6 +12,12 @@
{% trans "Edit" %}
{% endif %}
+ {% if request.user.is_staff and staff_session %}
+
+
+
+ {% endif %}
-
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/delete.html b/src/pretix/control/templates/pretixcontrol/organizers/delete.html
new file mode 100644
index 000000000..cc0c5def9
--- /dev/null
+++ b/src/pretix/control/templates/pretixcontrol/organizers/delete.html
@@ -0,0 +1,45 @@
+{% extends "pretixcontrol/organizers/base.html" %}
+{% load i18n %}
+{% load bootstrap3 %}
+{% block content %}
+
{% trans "Delete organizer" %}
+ {% if request.organizer.allow_delete %}
+ {% bootstrap_form_errors form layout="inline" %}
+
+ {% blocktrans trimmed %}
+ This operation will destroy this organizer including all events, configuration, products, quotas,
+ questions, vouchers, lists, etc.
+ {% endblocktrans %}
+
+
+ {% blocktrans trimmed %}
+ This operation is irreversible and there is no way to bring your data back.
+ {% endblocktrans %}
+
+
+ {% else %}
+
+ {% trans "This organizer account can not be deleted as it already contains orders, invoices, or devices." %}
+
+
+ {% blocktrans trimmed %}
+ pretix does not allow deleting orders once they have been placed in order to be audit-proof and
+ trustable by financial authorities.
+ {% endblocktrans %}
+
+ {% endif %}
+{% endblock %}
diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py
index 01cb0af1e..6a09ae9ac 100644
--- a/src/pretix/control/urls.py
+++ b/src/pretix/control/urls.py
@@ -67,6 +67,7 @@ urlpatterns = [
url(r'^organizers/select2$', typeahead.organizer_select2, name='organizers.select2'),
url(r'^organizer/(?P[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'),
url(r'^organizer/(?P[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
+ url(r'^organizer/(?P[^/]+)/delete$', organizer.OrganizerDelete.as_view(), name='organizer.delete'),
url(r'^organizer/(?P[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(),
name='organizer.display'),
url(r'^organizer/(?P[^/]+)/devices$', organizer.DeviceListView.as_view(), name='organizer.devices'),
diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py
index 2b6690fe3..45d16c5c0 100644
--- a/src/pretix/control/views/organizer.py
+++ b/src/pretix/control/views/organizer.py
@@ -6,7 +6,7 @@ from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.core.files import File
from django.db import transaction
-from django.db.models import Count
+from django.db.models import Count, ProtectedError
from django.forms import inlineformset_factory
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect
@@ -23,10 +23,13 @@ from pretix.base.models.organizer import TeamAPIToken
from pretix.base.services.mail import SendMailException, mail
from pretix.control.forms.filter import OrganizerFilterForm
from pretix.control.forms.organizer import (
- DeviceForm, EventMetaPropertyForm, OrganizerDisplaySettingsForm,
- OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
+ DeviceForm, EventMetaPropertyForm, OrganizerDeleteForm,
+ OrganizerDisplaySettingsForm, OrganizerForm, OrganizerSettingsForm,
+ OrganizerUpdateForm, TeamForm,
+)
+from pretix.control.permissions import (
+ AdministratorPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
)
-from pretix.control.permissions import OrganizerPermissionRequiredMixin
from pretix.control.signals import nav_organizer
from pretix.control.views import PaginationMixin
from pretix.helpers.urls import build_absolute_uri
@@ -168,6 +171,47 @@ class OrganizerDisplaySettings(OrganizerSettingsFormView):
return self.get(request)
+class OrganizerDelete(AdministratorPermissionRequiredMixin, FormView):
+ model = Organizer
+ template_name = 'pretixcontrol/organizers/delete.html'
+ context_object_name = 'organizer'
+ form_class = OrganizerDeleteForm
+
+ def post(self, request, *args, **kwargs):
+ if not self.request.organizer.allow_delete():
+ messages.error(self.request, _('This organizer can not be deleted.'))
+ return self.get(self.request, *self.args, **self.kwargs)
+ return super().post(request, *args, **kwargs)
+
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs['organizer'] = self.request.organizer
+ return kwargs
+
+ def form_valid(self, form):
+ try:
+ with transaction.atomic():
+ self.request.user.log_action(
+ 'pretix.organizer.deleted', user=self.request.user,
+ data={
+ 'organizer_id': self.request.organizer.pk,
+ 'name': str(self.request.organizer.name),
+ 'logentries': list(self.request.organizer.all_logentries().values_list('pk', flat=True))
+ }
+ )
+ self.request.organizer.delete_sub_objects()
+ self.request.organizer.delete()
+ messages.success(self.request, _('The organizer has been deleted.'))
+ return redirect(self.get_success_url())
+ except ProtectedError:
+ messages.error(self.request, _('The organizer could not be deleted as some constraints (e.g. data created by '
+ 'plug-ins) do not allow it.'))
+ return self.get(self.request, *self.args, **self.kwargs)
+
+ def get_success_url(self) -> str:
+ return reverse('control:index')
+
+
class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
model = Organizer
form_class = OrganizerUpdateForm