Make it possible to fully delete unlisted/unrated extensions #81
@ -236,11 +236,32 @@ class Extension(
|
|||||||
self.status = self.STATUSES.APPROVED
|
self.status = self.STATUSES.APPROVED
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cannot_be_deleted_reasons(self) -> List[str]:
|
||||||
|
"""Return a list of reasons why this extension cannot be fully deleted."""
|
||||||
|
reasons = []
|
||||||
|
if self.ratings.count() > 0:
|
||||||
|
reasons.append('has_ratings')
|
||||||
|
if self.is_listed:
|
||||||
|
reasons.append('is_listed')
|
||||||
|
return reasons
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def delete(self, hard=False):
|
def delete(self):
|
||||||
versions = self.versions.filter(date_deleted__isnull=True)
|
hard = self.cannot_be_deleted_reasons == []
|
||||||
|
versions = self.versions.all()
|
||||||
|
previews = self.previews.all()
|
||||||
|
# When soft-deleting, filter out already soft-deleted versions and previews
|
||||||
|
if not hard:
|
||||||
|
previews = self.previews.filter(date_deleted__isnull=True)
|
||||||
|
versions = self.versions.filter(date_deleted__isnull=True)
|
||||||
|
mode = not hard and 'soft-' or ''
|
||||||
|
for p in previews:
|
||||||
|
p.delete(hard=hard)
|
||||||
|
log.warning('%(mode)sdeleting preview file pk=% source=%s', mode, p.pk, p.source.name)
|
||||||
for v in versions:
|
for v in versions:
|
||||||
v.delete(hard=hard)
|
v.delete(hard=hard)
|
||||||
|
log.warning('%(mode)sdeleting extension pk=%s%sdeleted', mode, self.pk)
|
||||||
super().delete(hard=hard)
|
super().delete(hard=hard)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
@ -542,6 +563,22 @@ class Version(CreatedModifiedMixin, RatingMixin, TrackChangesMixin, SoftDeleteMi
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def delete(self, hard=False):
|
||||||
|
file = self.file
|
||||||
|
mode = not hard and 'soft-' or ''
|
||||||
|
args = {
|
||||||
|
'extension_id': self.extension_id,
|
||||||
|
'file_id': file.pk,
|
||||||
|
'mode': mode,
|
||||||
|
'source': file.source.name,
|
||||||
|
'version_id': self.pk,
|
||||||
|
'version': self.version,
|
||||||
|
}
|
||||||
|
log.warning('%(mode)sdeleting file pk=%(file_id)s source=%(source)s', args)
|
||||||
|
file.delete(hard=hard)
|
||||||
|
log.warning('%(mode)sdeleting version pk=%(version_id)s "%(version)s"', args)
|
||||||
|
super().delete(hard=hard)
|
||||||
|
|
||||||
def set_initial_permissions(self, _permissions):
|
def set_initial_permissions(self, _permissions):
|
||||||
if not _permissions:
|
if not _permissions:
|
||||||
return
|
return
|
||||||
|
@ -90,6 +90,11 @@
|
|||||||
<i class="i-send"></i>
|
<i class="i-send"></i>
|
||||||
<span>{% trans 'Submit for Approval' %}</span>
|
<span>{% trans 'Submit for Approval' %}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<a href="{{ extension.get_delete_url }}" class="btn btn-danger">
|
||||||
|
<i class="i-trash"></i>
|
||||||
|
<span>{% trans 'Delete Extension' %}</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from common.tests.factories.extensions import create_approved_version
|
from common.tests.factories.extensions import create_approved_version, create_version
|
||||||
|
from common.tests.factories.files import FileFactory
|
||||||
from common.tests.factories.users import UserFactory
|
from common.tests.factories.users import UserFactory
|
||||||
|
import extensions.models
|
||||||
|
import files.models
|
||||||
|
|
||||||
|
|
||||||
class DeleteTest(TestCase):
|
class DeleteTest(TestCase):
|
||||||
@ -32,3 +35,39 @@ class DeleteTest(TestCase):
|
|||||||
extension.refresh_from_db()
|
extension.refresh_from_db()
|
||||||
self.assertIsNone(extension.date_deleted)
|
self.assertIsNone(extension.date_deleted)
|
||||||
self.assertTrue(all(v.date_deleted is None for v in extension.versions.all()))
|
self.assertTrue(all(v.date_deleted is None for v in extension.versions.all()))
|
||||||
|
|
||||||
|
def test_incomplete_unrated_extension_can_be_fully_deleted_by_author(self):
|
||||||
|
version = create_version(
|
||||||
|
ratings=[],
|
||||||
|
extension__previews=[
|
||||||
|
FileFactory(
|
||||||
|
type=files.models.File.TYPES.IMAGE,
|
||||||
|
source='images/b0/b03fa981527593fbe15b28cf37c020220c3d83021999eab036b87f3bca9c9168.png',
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
extension = version.extension
|
||||||
|
version_file = version.file
|
||||||
|
self.assertEqual(extension.status, extension.STATUSES.INCOMPLETE)
|
||||||
|
preview_file = extension.previews.first()
|
||||||
|
self.assertIsNotNone(preview_file)
|
||||||
|
|
||||||
|
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)
|
||||||
|
# All relevant records should have been deleted
|
||||||
|
with self.assertRaises(extensions.models.Extension.DoesNotExist):
|
||||||
|
extension.refresh_from_db()
|
||||||
|
with self.assertRaises(extensions.models.Version.DoesNotExist):
|
||||||
|
version.refresh_from_db()
|
||||||
|
with self.assertRaises(files.models.File.DoesNotExist):
|
||||||
|
version_file.refresh_from_db()
|
||||||
|
with self.assertRaises(files.models.File.DoesNotExist):
|
||||||
|
preview_file.refresh_from_db()
|
||||||
|
self.assertIsNone(extensions.models.Extension.objects.filter(pk=extension.pk).first())
|
||||||
|
self.assertIsNone(extensions.models.Version.objects.filter(pk=version.pk).first())
|
||||||
|
self.assertIsNone(files.models.File.objects.filter(pk=version_file.pk).first())
|
||||||
|
self.assertIsNone(files.models.File.objects.filter(pk=preview_file.pk).first())
|
||||||
|
Loading…
Reference in New Issue
Block a user