Allow to delete organizers

This commit is contained in:
Raphael Michel
2018-11-05 10:55:06 +01:00
parent 87f3318431
commit 0a5347c08b
8 changed files with 143 additions and 9 deletions

View File

@@ -282,10 +282,10 @@ class Event(EventMixin, LoggedModel):
if not really: if not really:
raise TypeError("Pass really=True as a parameter.") raise TypeError("Pass really=True as a parameter.")
OrderPosition.objects.all().delete(order__event=self) OrderPosition.objects.filter(order__event=self).delete()
OrderFee.objects.all().delete(order__event=self) OrderFee.objects.filter(order__event=self).delete()
OrderPayment.objects.all().delete(order__event=self) OrderPayment.objects.filter(order__event=self).delete()
OrderRefund.objects.all().delete(order__event=self) OrderRefund.objects.filter(order__event=self).delete()
self.orders.all().delete() self.orders.all().delete()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@@ -301,7 +301,7 @@ class Event(EventMixin, LoggedModel):
return [] return []
return self.plugins.split(",") 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 Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
Django's built-in cache backends, but puts you into an isolated environment for Django's built-in cache backends, but puts you into an isolated environment for

View File

@@ -82,6 +82,20 @@ class Organizer(LoggedModel):
return ObjectRelatedCache(self) 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(): def generate_invite_token():
return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits) return get_random_string(length=32, allowed_chars=string.ascii_lowercase + string.digits)

View File

@@ -31,6 +31,29 @@ class OrganizerForm(I18nModelForm):
return slug 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): class OrganizerUpdateForm(OrganizerForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@@ -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.enabled': _('Notifications have been enabled.'),
'pretix.user.settings.notifications.disabled': _('Notifications have been disabled.'), 'pretix.user.settings.notifications.disabled': _('Notifications have been disabled.'),
'pretix.user.settings.notifications.changed': _('Your notification settings have been changed.'), '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 ' 'pretix.user.oauth.authorized': _('The application "{application_name}" has been authorized to access your '
'account.'), 'account.'),
'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'), 'pretix.control.auth.user.forgot_password.mail_sent': _('Password reset mail sent.'),

View File

@@ -12,6 +12,12 @@
{% trans "Edit" %} {% trans "Edit" %}
</a> </a>
{% endif %} {% endif %}
{% if request.user.is_staff and staff_session %}
<a href="{% url "control:organizer.delete" organizer=organizer.slug %}"
class="btn btn-danger hidden-print">
<span class="fa fa-trash"></span>
</a>
{% endif %}
</h1> </h1>
<ul class="nav nav-pills hidden-print"> <ul class="nav nav-pills hidden-print">
<li {% if "organizer" == url_name %}class="active"{% endif %}> <li {% if "organizer" == url_name %}class="active"{% endif %}>

View File

@@ -0,0 +1,45 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block content %}
<h1>{% trans "Delete organizer" %}</h1>
{% if request.organizer.allow_delete %}
{% bootstrap_form_errors form layout="inline" %}
<p>
{% blocktrans trimmed %}
This operation will destroy this organizer including all events, configuration, products, quotas,
questions, vouchers, lists, etc.
{% endblocktrans %}
</p>
<p><strong>
{% blocktrans trimmed %}
This operation is irreversible and there is no way to bring your data back.
{% endblocktrans %}
</strong></p>
<form action="" method="post">
{% csrf_token %}
<p>
{% blocktrans trimmed with slug=request.organizer.slug %}
To confirm you really want this, please type out the organizer's short name ("{{ slug }}") here:
{% endblocktrans %}
</p>
{% bootstrap_field form.slug layout="inline" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-danger btn-save">
{% trans "Delete" %}
</button>
</div>
</form>
{% else %}
<p>
{% trans "This organizer account can not be deleted as it already contains orders, invoices, or devices." %}
</p>
<p>
{% 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 %}
</p>
{% endif %}
{% endblock %}

View File

@@ -67,6 +67,7 @@ urlpatterns = [
url(r'^organizers/select2$', typeahead.organizer_select2, name='organizers.select2'), url(r'^organizers/select2$', typeahead.organizer_select2, name='organizers.select2'),
url(r'^organizer/(?P<organizer>[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'), url(r'^organizer/(?P<organizer>[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'),
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'), url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
url(r'^organizer/(?P<organizer>[^/]+)/delete$', organizer.OrganizerDelete.as_view(), name='organizer.delete'),
url(r'^organizer/(?P<organizer>[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(), url(r'^organizer/(?P<organizer>[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(),
name='organizer.display'), name='organizer.display'),
url(r'^organizer/(?P<organizer>[^/]+)/devices$', organizer.DeviceListView.as_view(), name='organizer.devices'), url(r'^organizer/(?P<organizer>[^/]+)/devices$', organizer.DeviceListView.as_view(), name='organizer.devices'),

View File

@@ -6,7 +6,7 @@ from django.contrib import messages
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.files import File from django.core.files import File
from django.db import transaction 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.forms import inlineformset_factory
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect 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.base.services.mail import SendMailException, mail
from pretix.control.forms.filter import OrganizerFilterForm from pretix.control.forms.filter import OrganizerFilterForm
from pretix.control.forms.organizer import ( from pretix.control.forms.organizer import (
DeviceForm, EventMetaPropertyForm, OrganizerDisplaySettingsForm, DeviceForm, EventMetaPropertyForm, OrganizerDeleteForm,
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm, 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.signals import nav_organizer
from pretix.control.views import PaginationMixin from pretix.control.views import PaginationMixin
from pretix.helpers.urls import build_absolute_uri from pretix.helpers.urls import build_absolute_uri
@@ -168,6 +171,47 @@ class OrganizerDisplaySettings(OrganizerSettingsFormView):
return self.get(request) 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): class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
model = Organizer model = Organizer
form_class = OrganizerUpdateForm form_class = OrganizerUpdateForm