From 0b12b7aa89803ea7d535b225e51c68b1d3d75b0e Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 29 Jan 2018 12:06:49 +0100 Subject: [PATCH] Refs #678 -- Allow deletion of events that do not have any orders --- doc/user/faq.rst | 8 ++- src/pretix/base/models/event.py | 3 + src/pretix/base/models/log.py | 2 +- src/pretix/control/forms/event.py | 41 +++++++++++ src/pretix/control/logdisplay.py | 1 + .../templates/pretixcontrol/event/delete.html | 70 +++++++++++++++++++ .../pretixcontrol/event/settings.html | 4 ++ src/pretix/control/urls.py | 1 + src/pretix/control/views/event.py | 47 ++++++++++++- src/tests/control/test_events.py | 58 +++++++++++++++ src/tests/control/test_permissions.py | 3 + 11 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 src/pretix/control/templates/pretixcontrol/event/delete.html diff --git a/doc/user/faq.rst b/doc/user/faq.rst index 2cf2ded7f..89e0a1034 100644 --- a/doc/user/faq.rst +++ b/doc/user/faq.rst @@ -24,9 +24,11 @@ received any real orders (i.e. taken the shop public). We won't charge any fees How do I delete an event? ------------------------- -It is currently not possible to delete events, you can just disable the shop by clicking the first square on your event -dashboard. Events can't be deleted as they most likely contain information on financial transactions which legally -needs to be kept on record for multiple years in most countries. +You can find the event deletion button at the bottom of the event settings page. Note however, that it is not possible +to delete an event once any order or invoice has been created, as those likely contain information on financial +transactions which legally may not be tampered with and needs to be kept on record for multiple years in most +countries. In this case, you can just disable the shop by clicking the first square on your event +dashboard. If you are using the hosted service at pretix.eu and want to get rid of an event that you only used for testing, contact us at support@pretix.eu and we can remove it for you. diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 3063037db..480713dc0 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -544,6 +544,9 @@ class Event(EventMixin, LoggedModel): Q(is_superuser=True) | Q(twp=True) ) + def allow_delete(self): + return not self.orders.exists() and not self.invoices.exists() + class SubEvent(EventMixin, LoggedModel): """ diff --git a/src/pretix/base/models/log.py b/src/pretix/base/models/log.py index 023cbd8b8..ec5bade41 100644 --- a/src/pretix/base/models/log.py +++ b/src/pretix/base/models/log.py @@ -41,7 +41,7 @@ class LogEntry(models.Model): datetime = models.DateTimeField(auto_now_add=True, db_index=True) user = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT) api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT) - event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.CASCADE) + event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL) action_type = models.CharField(max_length=255) data = models.TextField(default='{}') visible = models.BooleanField(default=True) diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 8abe13f61..649677e64 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -1,5 +1,6 @@ from django import forms from django.conf import settings +from django.contrib.auth.hashers import check_password from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.db.models import Q @@ -951,3 +952,43 @@ class WidgetCodeForm(forms.Form): raise ValidationError(_('The given voucher code does not exist.')) return v + + +class EventDeleteForm(forms.Form): + error_messages = { + 'pw_current_wrong': _("The password you entered was not correct."), + 'slug_wrong': _("The slug you entered was not correct."), + } + user_pw = forms.CharField( + max_length=255, + label=_("New password"), + widget=forms.PasswordInput() + ) + slug = forms.CharField( + max_length=255, + label=_("Event slug"), + ) + + def __init__(self, *args, **kwargs): + self.event = kwargs.pop('event') + self.user = kwargs.pop('user') + super().__init__(*args, **kwargs) + + def clean_user_pw(self): + user_pw = self.cleaned_data.get('user_pw') + if not check_password(user_pw, self.user.password): + raise forms.ValidationError( + self.error_messages['pw_current_wrong'], + code='pw_current_wrong', + ) + + return user_pw + + def clean_slug(self): + slug = self.cleaned_data.get('slug') + if slug != self.event.slug: + raise forms.ValidationError( + self.error_messages['slug_wrong'], + code='slug_wrong', + ) + return slug diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 3b4bb0cb5..46ab30cc1 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -137,6 +137,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): 'pretix.event.order.email.order_paid': _('An email has been sent to notify the user that payment has been received.'), 'pretix.event.order.email.order_placed': _('An email has been sent to notify the user that the order has been received and requires payment.'), 'pretix.event.order.email.resend': _('An email with a link to the order detail page has been resent to the user.'), + 'pretix.control.auth.user.created': _('The user has been created.'), 'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'), 'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'), 'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'), diff --git a/src/pretix/control/templates/pretixcontrol/event/delete.html b/src/pretix/control/templates/pretixcontrol/event/delete.html new file mode 100644 index 000000000..c36e79330 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/event/delete.html @@ -0,0 +1,70 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block content %} +

{% trans "Delete event" %}

+ {% if request.event.allow_delete %} + {% bootstrap_form_errors form layout="inline" %} +

+ {% blocktrans trimmed %} + This operation will destroy your event including all 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 %} +

+
+ {% csrf_token %} +

+ {% blocktrans trimmed with slug=request.event.slug %} + To confirm you really want this, please type out the event's short name ("{{ slug }}") here: + {% endblocktrans %} +

+ {% bootstrap_field form.slug layout="inline" %} +

+ {% blocktrans trimmed with slug=request.event.slug %} + Also, to make sure it's really you, please enter your user password here: + {% endblocktrans %} +

+ {% bootstrap_field form.user_pw layout="inline" %} + +
+ +
+
+ {% else %} +

+ {% trans "Your event can not be deleted as it already contains orders." %} +

+

+ {% 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 %} +

+ {% if request.event.live %} +

+ {% trans "You can instead take your shop offline. This will hide it from everyone except from the organizer teams you configured to have access to the event." %} +

+
+ {% csrf_token %} + + +
+ +
+
+ {% else %} +

+ {% trans "However, since your shop is offline, it is only visible to the organizing team according to the permissions you configured." %} +

+ {% endif %} + {% endif %} +{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index 9d3bc99c6..de506669d 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -78,6 +78,10 @@ + + {% trans "Delete event" %} + {% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index d099abe20..77fc34053 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -66,6 +66,7 @@ urlpatterns = [ url(r'^$', dashboards.event_index, name='event.index'), url(r'^live/$', event.EventLive.as_view(), name='event.live'), url(r'^logs/$', event.EventLog.as_view(), name='event.log'), + url(r'^delete/$', event.EventDelete.as_view(), name='event.delete'), url(r'^requiredactions/$', event.EventActions.as_view(), name='event.requiredactions'), url(r'^requiredactions/(?P\d+)/discard$', event.EventActionDiscard.as_view(), name='event.requiredaction.discard'), diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index cb08e0796..448367405 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -9,6 +9,7 @@ from django.contrib.contenttypes.models import ContentType from django.core.files import File from django.core.urlresolvers import reverse from django.db import transaction +from django.db.models import ProtectedError from django.http import ( Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, JsonResponse, @@ -34,8 +35,8 @@ from pretix.base.services import tickets from pretix.base.services.invoices import build_preview_invoice_pdf from pretix.base.signals import event_live_issues, register_ticket_outputs from pretix.control.forms.event import ( - CommentForm, DisplaySettingsForm, EventMetaValueForm, EventSettingsForm, - EventUpdateForm, InvoiceSettingsForm, MailSettingsForm, + CommentForm, DisplaySettingsForm, EventDeleteForm, EventMetaValueForm, + EventSettingsForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm, PaymentSettingsForm, ProviderForm, TaxRuleForm, TicketSettingsForm, WidgetCodeForm, ) @@ -786,6 +787,48 @@ class EventLive(EventPermissionRequiredMixin, TemplateView): }) +class EventDelete(EventPermissionRequiredMixin, FormView): + permission = 'can_change_event_settings' + template_name = 'pretixcontrol/event/delete.html' + form_class = EventDeleteForm + + def post(self, request, *args, **kwargs): + if not self.request.event.allow_delete(): + messages.error(self.request, _('This event 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['user'] = self.request.user + kwargs['event'] = self.request.event + return kwargs + + def form_valid(self, form): + try: + with transaction.atomic(): + self.request.organizer.log_action( + 'pretix.event.deleted', user=self.request.user, + data={ + 'event_id': self.request.event.pk, + 'name': str(self.request.event.name), + 'logentries': list(self.request.event.logentry_set.values_list('pk', flat=True)) + } + ) + self.request.event.items.all().delete() + self.request.event.subevents.all().delete() + self.request.event.delete() + messages.success(self.request, _('The event has been deleted.')) + return redirect(self.get_success_url()) + except ProtectedError: + messages.error(self.request, _('The event 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 EventLog(EventPermissionRequiredMixin, ListView): template_name = 'pretixcontrol/event/logs.html' model = LogEntry diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index f4e860dab..2e8aea867 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -724,3 +724,61 @@ class SubEventsTest(SoupTest): doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, {}, follow=True) assert doc.select(".alert-danger") assert self.event1.subevents.filter(pk=self.subevent1.pk).exists() + + +class EventDeletionTest(SoupTest): + def setUp(self): + super().setUp() + self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy') + self.orga1 = Organizer.objects.create(name='CCC', slug='ccc') + self.event1 = Event.objects.create( + organizer=self.orga1, name='30C3', slug='30c3', + date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc), + plugins='pretix.plugins.banktransfer,tests.testdummy', + has_subevents=False + ) + + t = Team.objects.create(organizer=self.orga1, can_create_events=True, can_change_event_settings=True, + can_change_items=True) + t.members.add(self.user) + t.limit_events.add(self.event1) + self.ticket = self.event1.items.create(name='Early-bird ticket', + category=None, default_price=23, + admission=True) + + self.client.login(email='dummy@dummy.dummy', password='dummy') + + def test_delete_allowed(self): + self.client.post('/control/event/ccc/30c3/delete/', { + 'user_pw': 'dummy', + 'slug': '30c3' + }) + + assert not self.orga1.events.exists() + + def test_delete_wrong_slug(self): + self.post_doc('/control/event/ccc/30c3/delete/', { + 'user_pw': 'dummy', + 'slug': '31c3' + }) + assert self.orga1.events.exists() + + def test_delete_wrong_pw(self): + self.post_doc('/control/event/ccc/30c3/delete/', { + 'user_pw': 'invalid', + 'slug': '30c3' + }) + assert self.orga1.events.exists() + + def test_delete_orders(self): + Order.objects.create( + code='FOO', event=self.event1, email='dummy@dummy.test', + status=Order.STATUS_PENDING, + datetime=now(), expires=now(), + total=14, payment_provider='banktransfer', locale='en' + ) + self.post_doc('/control/event/ccc/30c3/delete/', { + 'user_pw': 'dummy', + 'slug': '30c3' + }) + assert self.orga1.events.exists() diff --git a/src/tests/control/test_permissions.py b/src/tests/control/test_permissions.py index 087b30a15..6fdfcf93b 100644 --- a/src/tests/control/test_permissions.py +++ b/src/tests/control/test_permissions.py @@ -38,6 +38,8 @@ superuser_urls = [ event_urls = [ "", "comment/", + "live/", + "delete/", "settings/", "settings/plugins", "settings/payment", @@ -173,6 +175,7 @@ def test_wrong_event(perf_patch, client, env, url): event_permission_urls = [ ("can_change_event_settings", "live/", 200), + ("can_change_event_settings", "delete/", 200), ("can_change_event_settings", "settings/", 200), ("can_change_event_settings", "settings/plugins", 200), ("can_change_event_settings", "settings/payment", 200),