support deleting extensions #69

Merged
Oleg-Komarov merged 8 commits from delete-extension into main 2024-04-05 19:11:05 +02:00
8 changed files with 112 additions and 8 deletions
Showing only changes of commit 1cab51193b - Show all commits

View File

@ -166,13 +166,13 @@ class SoftDeleteMixin(models.Model):
if hard:
super().delete()
else:
if hasattr(self, 'file'):
# TODO(oleg) move to an archived directory, add random suffix
self.file.delete()
self.date_deleted = timezone.now()
self.save()
if hasattr(self, 'file'):
# .file should always exist but we don't want to break delete regardless
self.file.delete()
logger.warning('%r pk=%r deleted', self.__class__, self.pk)
logger.info('%r pk=%r deleted', self.__class__, self.pk)
def delete_queryset(self, request, queryset):
"""Given a queryset, soft-delete it from the database."""
@ -186,4 +186,4 @@ class SoftDeleteMixin(models.Model):
if save:
self.save()
logger.warning('%r pk=%r deleted', self.__class__, self.pk)
logger.info('%r pk=%r undeleted', self.__class__, self.pk)

View File

@ -109,6 +109,12 @@ class ExtensionUpdateForm(forms.ModelForm):
)
class ExtensionDeleteForm(forms.ModelForm):
class Meta:
model = extensions.models.Extension
fields = []
class VersionForm(forms.ModelForm):
class Meta:
model = extensions.models.Version
@ -129,7 +135,7 @@ class VersionForm(forms.ModelForm):
return self.initial['file']
class DeleteViewForm(forms.ModelForm):
class VersionDeleteForm(forms.ModelForm):
class Meta:
model = extensions.models.Version
fields = []

View File

@ -234,6 +234,13 @@ class Extension(
self.status = self.STATUSES.APPROVED
self.save()
@transaction.atomic
def delete(self, hard=False):
versions = self.versions.filter(date_deleted__isnull=True)
for v in versions:
v.delete(hard=hard)
super().delete(hard=hard)
def get_absolute_url(self):
return reverse('extensions:detail', args=[self.type_slug, self.slug])
@ -246,6 +253,9 @@ class Extension(
def get_manage_versions_url(self):
return reverse('extensions:manage-versions', args=[self.type_slug, self.slug])
def get_delete_url(self):
return reverse('extensions:delete', args=[self.type_slug, self.slug])
def get_new_version_url(self):
return reverse('extensions:new-version', args=[self.type_slug, self.slug])

View File

@ -0,0 +1,32 @@
{% extends "common/base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-md-8 mx-auto my-4">
<div class="box">
<h2>
{% blocktranslate with extension_name=extension_name %} Delete {{ extension_name }}?{% endblocktranslate %}
</h2>
<p>
{% blocktranslate with extension_name=extension_name %}
By deleting <strong>{{ extension_name }}</strong> you will lose the files,
download count, reviews as well ratings for the whole extension.
{% endblocktranslate %}
</p>
<div class="btn-row-fluid">
<a href="#" class="btn js-btn-back">
<i class="i-cancel"></i>
<span>{% trans 'Cancel' %}</span>
</a>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-block btn-danger">
<i class="i-trash"></i>
<span>{% trans 'Confirm Deletion' %}</span>
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -115,6 +115,11 @@
<span>{% trans 'Version History' %}</span>
</a>
<a href="{{ extension.get_delete_url }}" class="btn btn-danger">
<i class="i-trash"></i>
<span>{% trans 'Delete Extension' %}</span>
</a>
{% if request.user.is_staff %}
<a href="{% url 'admin:extensions_extension_change' extension.pk %}" class="btn btn-admin">
<span>Admin</span>

View File

@ -0,0 +1,20 @@
from django.test import TestCase
from common.tests.factories.extensions import create_approved_version
class DeleteTest(TestCase):
fixtures = ['dev', 'licenses']
def test_happy_path(self):
extension = create_approved_version().extension
url = extension.get_delete_url()
user = extension.authors.first()
self.client.force_login(user)
response = self.client.post(url)
self.assertEqual(response.status_code, 302)
extension.refresh_from_db()
self.assertIsNotNone(extension.date_deleted)
self.assertTrue(all(v.date_deleted is not None for v in extension.versions.all()))

View File

@ -44,6 +44,11 @@ urlpatterns = [
manage.UpdateExtensionView.as_view(),
name='manage',
),
path(
'<slug:slug>/delete/',
manage.DeleteExtensionView.as_view(),
name='delete',
),
path(
'<slug:slug>/manage/versions/',
manage.ManageVersionsView.as_view(),

View File

@ -18,9 +18,10 @@ from .mixins import (
from extensions.forms import (
EditPreviewFormSet,
AddPreviewFormSet,
ExtensionDeleteForm,
ExtensionUpdateForm,
VersionForm,
DeleteViewForm,
VersionDeleteForm,
)
from extensions.models import Extension, Version
from files.forms import FileForm
@ -159,6 +160,31 @@ class UpdateExtensionView(
return self.form_invalid(form, edit_preview_formset, add_preview_formset)
class DeleteExtensionView(
LoginRequiredMixin,
MaintainedExtensionMixin,
DeleteView,
):
model = Extension
template_name = 'extensions/confirm_delete.html'
form_class = ExtensionDeleteForm
def get_success_url(self):
return reverse('extensions:manage-list')
def get_object(self, queryset=None):
return get_object_or_404(
Extension,
slug=self.kwargs['slug'],
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['extension_name'] = self.object.name
context['confirm_url'] = self.object.get_delete_url()
return context
class VersionDeleteView(
LoginRequiredMixin,
MaintainedExtensionMixin,
@ -166,7 +192,7 @@ class VersionDeleteView(
):
model = Version
template_name = 'extensions/version_confirm_delete.html'
form_class = DeleteViewForm
form_class = VersionDeleteForm
def get_success_url(self):
return reverse(