Skip to content

Commit d51f806

Browse files
polycconfsbraun
andauthored
feat: Added bulk delete to version change view (#338)
* test: adds bulk delete failing test * feat: adds first functional draft of delete_selected method * test: adds new test case to check there is warning when published version is selected * Update test_admin.py for non sqlite testing * Add error messages, and update delete permission to include content object * fix: bugs in test_admin * Update test to new expectation: Do not delete anything if a published or draft version is amongst the selected objects * Delegate the content delete to the `delete_selected` method Update the queryset to contain content elements * Add test for confirmation message * Update tests/test_admin.py * Update admin.py * Update test_admin.py * Update test_admin.py * Update admin.py * Update djangocms_versioning/admin.py --------- Co-authored-by: Fabian Braun <[email protected]>
1 parent 76a7cc4 commit d51f806

File tree

2 files changed

+125
-12
lines changed

2 files changed

+125
-12
lines changed

djangocms_versioning/admin.py

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
from cms.utils.urlutils import add_url_parameters, static_with_version
1313
from django.conf import settings
1414
from django.contrib import admin, messages
15+
from django.contrib.admin.actions import delete_selected
1516
from django.contrib.admin.options import IncorrectLookupParameters
1617
from django.contrib.admin.utils import unquote
1718
from django.contrib.admin.views.main import ChangeList
1819
from django.contrib.contenttypes.models import ContentType
19-
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
20+
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
2021
from django.db import models
2122
from django.db.models import OuterRef, Subquery
2223
from django.db.models.functions import Cast, Lower
@@ -614,7 +615,7 @@ class VersionAdmin(ChangeListActionsMixin, admin.ModelAdmin, metaclass=MediaDefi
614615
"""
615616

616617
# register custom actions
617-
actions = ["compare_versions"]
618+
actions = ["compare_versions", "delete_selected"]
618619
list_display = (
619620
"number",
620621
"created",
@@ -649,14 +650,6 @@ def get_list_filter(self, request):
649650
for field in versionable.extra_grouping_fields
650651
]
651652

652-
def get_actions(self, request):
653-
"""Removes the standard django admin delete action."""
654-
actions = super().get_actions(request)
655-
# disable delete action
656-
if "delete_selected" in actions and not conf.ALLOW_DELETING_VERSIONS:
657-
del actions["delete_selected"]
658-
return actions
659-
660653
@admin.display(
661654
description=_("Content"),
662655
ordering="content",
@@ -927,6 +920,44 @@ def compare_versions(self, request, queryset):
927920

928921
return redirect(url)
929922

923+
def delete_view(self, request, object_id, extra_context=None):
924+
"""Do not allow deleting single version objects. Use discard instead."""
925+
raise PermissionDenied
926+
927+
@admin.action(
928+
permissions=["delete"],
929+
description=_("Delete selected %(verbose_name_plural)s"),
930+
)
931+
def delete_selected(self, request, queryset):
932+
"""
933+
Redirects to a delete versions view based on a users choice
934+
"""
935+
# Do not allow deleting single version objects. Use discard instead.
936+
forbidden = queryset.filter(state__in=(PUBLISHED, DRAFT))
937+
if forbidden.exists():
938+
self.message_user(
939+
request,
940+
_("Draft or published versions cannot be deleted. First unpublish or use discard for drafts."),
941+
messages.ERROR
942+
)
943+
return None
944+
945+
if request.POST.get("post"):
946+
# When the user confirms, delete the content objects
947+
queryset = self.get_content_queryset(queryset)
948+
return delete_selected(self, request, queryset)
949+
950+
def get_deleted_objects(self, objs, request):
951+
"""Return the content objects to be deleted"""
952+
if issubclass(objs.model, Version):
953+
objs = self.get_content_queryset(objs)
954+
return super().get_deleted_objects(objs, request)
955+
956+
def get_content_queryset(self, queryset):
957+
return self.model._source_model._base_manager.filter(
958+
pk__in=queryset.values_list("object_id", flat=True)
959+
)
960+
930961
def grouper_form_view(self, request):
931962
"""Displays an intermediary page to select a grouper object
932963
to show versions of.
@@ -1388,7 +1419,7 @@ def changelist_view(self, request, extra_context=None):
13881419
.latest("created")
13891420
.content
13901421
)
1391-
except ObjectDoesNotExist:
1422+
except (ObjectDoesNotExist, KeyError):
13921423
pass
13931424
return response
13941425

@@ -1452,4 +1483,11 @@ def has_change_permission(self, request, obj=None):
14521483
return super().has_change_permission(request, obj)
14531484

14541485
def has_delete_permission(self, request, obj=None):
1455-
return False
1486+
if obj is None:
1487+
return conf.ALLOW_DELETING_VERSIONS and super().has_delete_permission(request, obj)
1488+
content_admin = self.admin_site._registry[self.model._source_model]
1489+
return all((
1490+
conf.ALLOW_DELETING_VERSIONS,
1491+
super().has_delete_permission(request, obj),
1492+
content_admin.has_delete_permission(request, obj.content),
1493+
))

tests/test_admin.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2652,6 +2652,81 @@ def test_change_view_action_compare_versions_three_selected(self):
26522652
self.assertContains(response, "Exactly two versions need to be selected.")
26532653

26542654

2655+
class VersionBulkDeleteViewTestCase(CMSTestCase):
2656+
def setUp(self):
2657+
self.versionable = PollsCMSConfig.versioning[0]
2658+
self.superuser = self.get_superuser()
2659+
2660+
@patch("djangocms_versioning.conf.ALLOW_DELETING_VERSIONS", True)
2661+
def test_change_view_action_bulk_delete_versions_three_selected(self):
2662+
"""
2663+
Query returns 1 versions when three versioning options are selected
2664+
to delete
2665+
"""
2666+
poll = factories.PollFactory()
2667+
versions = factories.PollVersionFactory.create_batch(4, content__poll=poll, state=constants.ARCHIVED)
2668+
querystring = f"?poll={poll.pk}"
2669+
endpoint = (
2670+
self.get_admin_url(self.versionable.version_model_proxy, "changelist")
2671+
+ querystring
2672+
)
2673+
2674+
with self.login_user_context(self.superuser):
2675+
data = {
2676+
"action": "delete_selected",
2677+
ACTION_CHECKBOX_NAME: [str(version.pk) for version in versions[1:]],
2678+
"post": "yes",
2679+
}
2680+
response = self.client.post(endpoint, data, follow=True)
2681+
2682+
self.assertEqual(response.status_code, 200)
2683+
self.assertEqual(PollContent._base_manager.all().count(), 1)
2684+
2685+
2686+
@patch("djangocms_versioning.conf.ALLOW_DELETING_VERSIONS", True)
2687+
def test_change_view_action_bulk_delete_versions_gives_warning_when_published_selected(self):
2688+
"""
2689+
Nothing is deleted if a published (or draft) version is amongst the selected objects
2690+
"""
2691+
poll = factories.PollFactory()
2692+
published = factories.PollVersionFactory(state=constants.PUBLISHED)
2693+
versions = factories.PollVersionFactory.create_batch(4, content__poll=poll)
2694+
querystring = f"?poll={poll.pk}"
2695+
endpoint = (
2696+
self.get_admin_url(self.versionable.version_model_proxy, "changelist")
2697+
+ querystring
2698+
)
2699+
2700+
with self.login_user_context(self.superuser):
2701+
data = {
2702+
"action": "delete_selected",
2703+
ACTION_CHECKBOX_NAME: [published.pk] + [version.pk for version in versions],
2704+
"post": "yes",
2705+
}
2706+
response = self.client.post(endpoint, data, follow=True)
2707+
2708+
self.assertEqual(response.status_code, 200)
2709+
self.assertEqual(PollContent._base_manager.all().count(), 1 + 4)
2710+
2711+
@patch("djangocms_versioning.conf.ALLOW_DELETING_VERSIONS", True)
2712+
def test_bulk_delete_action_confirmation(self):
2713+
version = factories.PollVersionFactory(state=constants.ARCHIVED)
2714+
url = self.get_admin_url(self.versionable.version_model_proxy, "changelist")
2715+
url += f"?poll={version.content.poll.pk}"
2716+
data = {
2717+
"action": "delete_selected",
2718+
ACTION_CHECKBOX_NAME: [version.pk],
2719+
}
2720+
with self.login_user_context(self.superuser):
2721+
response = self.client.post(url, data, follow=True)
2722+
2723+
# Check that the confirmation page is displayed
2724+
self.assertEqual(response.status_code, 200)
2725+
self.assertContains(response, "Are you sure you want to delete the selected poll content version?")
2726+
# Check that the poll content is contained in the confirmation
2727+
self.assertContains(response, str(version))
2728+
2729+
26552730
class ExtendedVersionAdminTestCase(CMSTestCase):
26562731

26572732
def test_extended_version_change_list_display_renders_from_provided_list_display(self):

0 commit comments

Comments
 (0)