Skip to content

Commit

Permalink
feat: Added bulk delete to version change view (#338)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
polyccon and fsbraun authored Oct 29, 2024
1 parent 76a7cc4 commit d51f806
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 12 deletions.
62 changes: 50 additions & 12 deletions djangocms_versioning/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
from cms.utils.urlutils import add_url_parameters, static_with_version
from django.conf import settings
from django.contrib import admin, messages
from django.contrib.admin.actions import delete_selected
from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.admin.utils import unquote
from django.contrib.admin.views.main import ChangeList
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
from django.db import models
from django.db.models import OuterRef, Subquery
from django.db.models.functions import Cast, Lower
Expand Down Expand Up @@ -614,7 +615,7 @@ class VersionAdmin(ChangeListActionsMixin, admin.ModelAdmin, metaclass=MediaDefi
"""

# register custom actions
actions = ["compare_versions"]
actions = ["compare_versions", "delete_selected"]
list_display = (
"number",
"created",
Expand Down Expand Up @@ -649,14 +650,6 @@ def get_list_filter(self, request):
for field in versionable.extra_grouping_fields
]

def get_actions(self, request):
"""Removes the standard django admin delete action."""
actions = super().get_actions(request)
# disable delete action
if "delete_selected" in actions and not conf.ALLOW_DELETING_VERSIONS:
del actions["delete_selected"]
return actions

@admin.display(
description=_("Content"),
ordering="content",
Expand Down Expand Up @@ -927,6 +920,44 @@ def compare_versions(self, request, queryset):

return redirect(url)

def delete_view(self, request, object_id, extra_context=None):
"""Do not allow deleting single version objects. Use discard instead."""
raise PermissionDenied

@admin.action(
permissions=["delete"],
description=_("Delete selected %(verbose_name_plural)s"),
)
def delete_selected(self, request, queryset):
"""
Redirects to a delete versions view based on a users choice
"""
# Do not allow deleting single version objects. Use discard instead.
forbidden = queryset.filter(state__in=(PUBLISHED, DRAFT))
if forbidden.exists():
self.message_user(
request,
_("Draft or published versions cannot be deleted. First unpublish or use discard for drafts."),
messages.ERROR
)
return None

if request.POST.get("post"):
# When the user confirms, delete the content objects
queryset = self.get_content_queryset(queryset)
return delete_selected(self, request, queryset)

def get_deleted_objects(self, objs, request):
"""Return the content objects to be deleted"""
if issubclass(objs.model, Version):
objs = self.get_content_queryset(objs)
return super().get_deleted_objects(objs, request)

def get_content_queryset(self, queryset):
return self.model._source_model._base_manager.filter(
pk__in=queryset.values_list("object_id", flat=True)
)

def grouper_form_view(self, request):
"""Displays an intermediary page to select a grouper object
to show versions of.
Expand Down Expand Up @@ -1388,7 +1419,7 @@ def changelist_view(self, request, extra_context=None):
.latest("created")
.content
)
except ObjectDoesNotExist:
except (ObjectDoesNotExist, KeyError):
pass
return response

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

def has_delete_permission(self, request, obj=None):
return False
if obj is None:
return conf.ALLOW_DELETING_VERSIONS and super().has_delete_permission(request, obj)
content_admin = self.admin_site._registry[self.model._source_model]
return all((
conf.ALLOW_DELETING_VERSIONS,
super().has_delete_permission(request, obj),
content_admin.has_delete_permission(request, obj.content),
))
75 changes: 75 additions & 0 deletions tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2652,6 +2652,81 @@ def test_change_view_action_compare_versions_three_selected(self):
self.assertContains(response, "Exactly two versions need to be selected.")


class VersionBulkDeleteViewTestCase(CMSTestCase):
def setUp(self):
self.versionable = PollsCMSConfig.versioning[0]
self.superuser = self.get_superuser()

@patch("djangocms_versioning.conf.ALLOW_DELETING_VERSIONS", True)
def test_change_view_action_bulk_delete_versions_three_selected(self):
"""
Query returns 1 versions when three versioning options are selected
to delete
"""
poll = factories.PollFactory()
versions = factories.PollVersionFactory.create_batch(4, content__poll=poll, state=constants.ARCHIVED)
querystring = f"?poll={poll.pk}"
endpoint = (
self.get_admin_url(self.versionable.version_model_proxy, "changelist")
+ querystring
)

with self.login_user_context(self.superuser):
data = {
"action": "delete_selected",
ACTION_CHECKBOX_NAME: [str(version.pk) for version in versions[1:]],
"post": "yes",
}
response = self.client.post(endpoint, data, follow=True)

self.assertEqual(response.status_code, 200)
self.assertEqual(PollContent._base_manager.all().count(), 1)


@patch("djangocms_versioning.conf.ALLOW_DELETING_VERSIONS", True)
def test_change_view_action_bulk_delete_versions_gives_warning_when_published_selected(self):
"""
Nothing is deleted if a published (or draft) version is amongst the selected objects
"""
poll = factories.PollFactory()
published = factories.PollVersionFactory(state=constants.PUBLISHED)
versions = factories.PollVersionFactory.create_batch(4, content__poll=poll)
querystring = f"?poll={poll.pk}"
endpoint = (
self.get_admin_url(self.versionable.version_model_proxy, "changelist")
+ querystring
)

with self.login_user_context(self.superuser):
data = {
"action": "delete_selected",
ACTION_CHECKBOX_NAME: [published.pk] + [version.pk for version in versions],
"post": "yes",
}
response = self.client.post(endpoint, data, follow=True)

self.assertEqual(response.status_code, 200)
self.assertEqual(PollContent._base_manager.all().count(), 1 + 4)

@patch("djangocms_versioning.conf.ALLOW_DELETING_VERSIONS", True)
def test_bulk_delete_action_confirmation(self):
version = factories.PollVersionFactory(state=constants.ARCHIVED)
url = self.get_admin_url(self.versionable.version_model_proxy, "changelist")
url += f"?poll={version.content.poll.pk}"
data = {
"action": "delete_selected",
ACTION_CHECKBOX_NAME: [version.pk],
}
with self.login_user_context(self.superuser):
response = self.client.post(url, data, follow=True)

# Check that the confirmation page is displayed
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Are you sure you want to delete the selected poll content version?")
# Check that the poll content is contained in the confirmation
self.assertContains(response, str(version))


class ExtendedVersionAdminTestCase(CMSTestCase):

def test_extended_version_change_list_display_renders_from_provided_list_display(self):
Expand Down

0 comments on commit d51f806

Please sign in to comment.