Refs #678 -- Allow deletion of events that do not have any orders

This commit is contained in:
Raphael Michel
2018-01-29 12:06:49 +01:00
parent 14da25bd9e
commit 0b12b7aa89
11 changed files with 232 additions and 6 deletions

View File

@@ -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? 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 You can find the event deletion button at the bottom of the event settings page. Note however, that it is not possible
dashboard. Events can't be deleted as they most likely contain information on financial transactions which legally to delete an event once any order or invoice has been created, as those likely contain information on financial
needs to be kept on record for multiple years in most countries. 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 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. us at support@pretix.eu and we can remove it for you.

View File

@@ -544,6 +544,9 @@ class Event(EventMixin, LoggedModel):
Q(is_superuser=True) | Q(twp=True) 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): class SubEvent(EventMixin, LoggedModel):
""" """

View File

@@ -41,7 +41,7 @@ class LogEntry(models.Model):
datetime = models.DateTimeField(auto_now_add=True, db_index=True) datetime = models.DateTimeField(auto_now_add=True, db_index=True)
user = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT) 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) 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) action_type = models.CharField(max_length=255)
data = models.TextField(default='{}') data = models.TextField(default='{}')
visible = models.BooleanField(default=True) visible = models.BooleanField(default=True)

View File

@@ -1,5 +1,6 @@
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.hashers import check_password
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db.models import Q from django.db.models import Q
@@ -951,3 +952,43 @@ class WidgetCodeForm(forms.Form):
raise ValidationError(_('The given voucher code does not exist.')) raise ValidationError(_('The given voucher code does not exist.'))
return v 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

View File

@@ -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_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.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.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.enabled': _('Two-factor authentication has been enabled.'),
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'), 'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'), 'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),

View File

@@ -0,0 +1,70 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block content %}
<h1>{% trans "Delete event" %}</h1>
{% if request.event.allow_delete %}
{% bootstrap_form_errors form layout="inline" %}
<p>
{% blocktrans trimmed %}
This operation will destroy your event including all 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.event.slug %}
To confirm you really want this, please type out the event's short name ("{{ slug }}") here:
{% endblocktrans %}
</p>
{% bootstrap_field form.slug layout="inline" %}
<p>
{% blocktrans trimmed with slug=request.event.slug %}
Also, to make sure it's really you, please enter your user password here:
{% endblocktrans %}
</p>
{% bootstrap_field form.user_pw 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 "Your event can not be deleted as it already contains orders." %}
</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>
{% if request.event.live %}
<p>
{% 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." %}
</p>
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="live" value="false">
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Go offline" %}
</button>
</div>
</form>
{% else %}
<p>
{% trans "However, since your shop is offline, it is only visible to the organizing team according to the permissions you configured." %}
</p>
{% endif %}
{% endif %}
{% endblock %}

View File

@@ -78,6 +78,10 @@
<button type="submit" class="btn btn-primary btn-save"> <button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %} {% trans "Save" %}
</button> </button>
<a href="{% url "control:event.delete" organizer=request.organizer.slug event=request.event.slug %}"
class="btn {% if request.event.allow_delete %}{% endif %} btn-danger btn-lg pull-left">
{% trans "Delete event" %}
</a>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}

View File

@@ -66,6 +66,7 @@ urlpatterns = [
url(r'^$', dashboards.event_index, name='event.index'), url(r'^$', dashboards.event_index, name='event.index'),
url(r'^live/$', event.EventLive.as_view(), name='event.live'), url(r'^live/$', event.EventLive.as_view(), name='event.live'),
url(r'^logs/$', event.EventLog.as_view(), name='event.log'), 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/$', event.EventActions.as_view(), name='event.requiredactions'),
url(r'^requiredactions/(?P<id>\d+)/discard$', event.EventActionDiscard.as_view(), url(r'^requiredactions/(?P<id>\d+)/discard$', event.EventActionDiscard.as_view(),
name='event.requiredaction.discard'), name='event.requiredaction.discard'),

View File

@@ -9,6 +9,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.files import File from django.core.files import File
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import transaction from django.db import transaction
from django.db.models import ProtectedError
from django.http import ( from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, Http404, HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed,
JsonResponse, JsonResponse,
@@ -34,8 +35,8 @@ from pretix.base.services import tickets
from pretix.base.services.invoices import build_preview_invoice_pdf from pretix.base.services.invoices import build_preview_invoice_pdf
from pretix.base.signals import event_live_issues, register_ticket_outputs from pretix.base.signals import event_live_issues, register_ticket_outputs
from pretix.control.forms.event import ( from pretix.control.forms.event import (
CommentForm, DisplaySettingsForm, EventMetaValueForm, EventSettingsForm, CommentForm, DisplaySettingsForm, EventDeleteForm, EventMetaValueForm,
EventUpdateForm, InvoiceSettingsForm, MailSettingsForm, EventSettingsForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm,
PaymentSettingsForm, ProviderForm, TaxRuleForm, TicketSettingsForm, PaymentSettingsForm, ProviderForm, TaxRuleForm, TicketSettingsForm,
WidgetCodeForm, 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): class EventLog(EventPermissionRequiredMixin, ListView):
template_name = 'pretixcontrol/event/logs.html' template_name = 'pretixcontrol/event/logs.html'
model = LogEntry model = LogEntry

View File

@@ -724,3 +724,61 @@ class SubEventsTest(SoupTest):
doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, {}, follow=True) doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/delete' % self.subevent1.pk, {}, follow=True)
assert doc.select(".alert-danger") assert doc.select(".alert-danger")
assert self.event1.subevents.filter(pk=self.subevent1.pk).exists() 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()

View File

@@ -38,6 +38,8 @@ superuser_urls = [
event_urls = [ event_urls = [
"", "",
"comment/", "comment/",
"live/",
"delete/",
"settings/", "settings/",
"settings/plugins", "settings/plugins",
"settings/payment", "settings/payment",
@@ -173,6 +175,7 @@ def test_wrong_event(perf_patch, client, env, url):
event_permission_urls = [ event_permission_urls = [
("can_change_event_settings", "live/", 200), ("can_change_event_settings", "live/", 200),
("can_change_event_settings", "delete/", 200),
("can_change_event_settings", "settings/", 200), ("can_change_event_settings", "settings/", 200),
("can_change_event_settings", "settings/plugins", 200), ("can_change_event_settings", "settings/plugins", 200),
("can_change_event_settings", "settings/payment", 200), ("can_change_event_settings", "settings/payment", 200),