From cf82b7f8c391177fe4657580df1de66ba1f4066c Mon Sep 17 00:00:00 2001
From: Jonathan Sundqvist <sundqvist.jonathan@gmail.com>
Date: Mon, 14 Oct 2019 13:26:59 +0200
Subject: [PATCH] Add unpublish_selected among actions

---
 djangocms_moderation/admin.py                 | 112 +++++++++++-------
 djangocms_moderation/admin_actions.py         |   9 +-
 .../publish_confirmation.html                 |   4 +
 tests/test_admin.py                           |  19 ++-
 4 files changed, 96 insertions(+), 48 deletions(-)

diff --git a/djangocms_moderation/admin.py b/djangocms_moderation/admin.py
index 902b45cf..194da776 100644
--- a/djangocms_moderation/admin.py
+++ b/djangocms_moderation/admin.py
@@ -29,6 +29,7 @@
     publish_selected,
     reject_selected,
     resubmit_selected,
+    unpublish_selected,
 )
 from .emails import notify_collection_author, notify_collection_moderators
 from .filters import ModeratorFilter, ReviewerFilter
@@ -119,6 +120,7 @@ class Media:
 
     actions = [  # filtered out in `self.get_actions`
         delete_selected,
+        unpublish_selected,
         publish_selected,
         approve_selected,
         reject_selected,
@@ -289,31 +291,50 @@ def get_actions(self, request):
                 )  # publish_selected, approve_selected, reject_selected, resubmit_selected
             else:
                 # If the collection is archived, then no other action than
-                # `publish_selected` is possible.
+                # `publish_selected` or `unpublish_selected` is possible.
                 _max_to_keep = 1  # publish_selected
 
             for mr in collection.moderation_requests.all().select_related("version"):
                 if len(actions_to_keep) == _max_to_keep:
                     break  # We have found all the actions, so no need to loop anymore
-                if "publish_selected" not in actions_to_keep:
-                    if (
-                        request.user == collection.author
-                        and mr.version_can_be_published()
-                    ):
-                        actions_to_keep.append("publish_selected")
-                if (
-                    collection.status == constants.IN_REVIEW
-                    and "approve_selected" not in actions_to_keep
-                ):
-                    if mr.user_can_take_moderation_action(request.user):
-                        actions_to_keep.append("approve_selected")
-                        actions_to_keep.append("reject_selected")
-                if (
-                    collection.status == constants.IN_REVIEW
-                    and "resubmit_selected" not in actions_to_keep
-                ):
-                    if mr.user_can_resubmit(request.user):
-                        actions_to_keep.append("resubmit_selected")
+
+                publish_condition = all([
+                    "publish_selected" not in actions_to_keep,
+                    request.user == collection.author,
+                    collection.workflow.is_unpublishing is False,
+                    mr.version_can_be_published()
+                ])
+
+                unpublish_condition = all([
+                    "unpublish_selected" not in actions_to_keep,
+                    collection.workflow.is_unpublishing is True,
+                    mr.version_can_be_unpublished()
+                ])
+
+                approve_condition = all([
+                    "approve_selected" not in actions_to_keep,
+                    collection.status == constants.IN_REVIEW,
+                    mr.user_can_take_moderation_action(request.user)
+                ])
+
+                resubmit_condition = all([
+                    "resubmit_selected" not in actions_to_keep,
+                    collection.status == constants.IN_REVIEW,
+                    mr.user_can_resubmit(request.user)
+                ])
+
+                if unpublish_condition:
+                    actions_to_keep.append("unpublish_selected")
+
+                if publish_condition:
+                    actions_to_keep.append("publish_selected")
+
+                if approve_condition:
+                    actions_to_keep.append("approve_selected")
+                    actions_to_keep.append("reject_selected")
+
+                if resubmit_condition:
+                    actions_to_keep.append("resubmit_selected")
 
         # Only collection author can delete moderation requests
         if collection.author == request.user:
@@ -495,31 +516,30 @@ def _get_selected_tree_nodes(self, request):
         ).select_related('moderation_request')
         return treenodes
 
-    def _custom_view_context(self, request):
+    def _custom_view_context(self, request, collection):
         treenodes = self._get_selected_tree_nodes(request)
-        collection_id = request.GET.get('collection_id')
-        redirect_url = self._redirect_to_changeview_url(collection_id)
+        redirect_url = self._redirect_to_changeview_url(collection.pk)
         return dict(
             ids=request.GET.getlist("ids"),
             back_url=redirect_url,
-            queryset=[n.moderation_request for n in treenodes]
+            queryset=[n.moderation_request for n in treenodes],
+            collection=collection
         )
 
     def resubmit_view(self, request):
         collection_id = request.GET.get('collection_id')
-        treenodes = self._get_selected_tree_nodes(request)
-        redirect_url = self._redirect_to_changeview_url(collection_id)
-
         try:
             collection = ModerationCollection.objects.get(id=int(collection_id))
         except (ValueError, ModerationCollection.DoesNotExist):
             raise Http404
+        treenodes = self._get_selected_tree_nodes(request)
+        redirect_url = self._redirect_to_changeview_url(collection_id)
 
         if collection.author != request.user:
             raise PermissionDenied
 
         if request.method != 'POST':
-            context = self._custom_view_context(request)
+            context = self._custom_view_context(request, collection)
             return render(
                 request,
                 'admin/djangocms_moderation/moderationrequest/resubmit_confirmation.html',
@@ -527,7 +547,6 @@ def resubmit_view(self, request):
             )
         else:
             resubmitted_requests = []
-
             for node in treenodes.all():
                 mr = node.moderation_request
                 if mr.user_can_resubmit(request.user):
@@ -568,7 +587,8 @@ def resubmit_view(self, request):
     def _publish_flow(self, request, queryset):
         """Handles the published workflow"""
         published_moderation_requests = []
-        for mr in queryset.all():
+        for node in queryset.all():
+            mr = node.moderation_request
             if mr.version_can_be_published():
                 mr.version.publish(request.user)
                 published_moderation_requests.append(mr)
@@ -589,7 +609,8 @@ def _publish_flow(self, request, queryset):
 
     def _unpublish_flow(self, request, queryset):
         unpublished_moderation_requests = []
-        for mr in queryset.all():
+        for node in queryset.all():
+            mr = node.moderation_request
             if mr.version_can_be_unpublished():
                 mr.version.unpublish(request.user)
                 unpublished_moderation_requests.append(mr)
@@ -610,13 +631,12 @@ def _unpublish_flow(self, request, queryset):
 
     def published_view(self, request):
         collection_id = request.GET.get('collection_id')
-        treenodes = self._get_selected_tree_nodes(request)
-        redirect_url = self._redirect_to_changeview_url(collection_id)
-
         try:
             collection = ModerationCollection.objects.get(id=int(collection_id))
         except (ValueError, ModerationCollection.DoesNotExist):
             raise Http404
+        treenodes = self._get_selected_tree_nodes(request)
+        redirect_url = self._redirect_to_changeview_url(collection_id)
 
         if request.user != collection.author:
             raise PermissionDenied
@@ -625,7 +645,7 @@ def published_view(self, request):
             return HttpResponseNotAllowed
 
         if request.method == 'GET':
-            context = self._custom_view_context(request)
+            context = self._custom_view_context(request, collection)
             return render(
                 request,
                 "admin/djangocms_moderation/moderationrequest/publish_confirmation.html",
@@ -649,24 +669,24 @@ def published_view(self, request):
 
     def rework_view(self, request):
         collection_id = request.GET.get('collection_id')
+        try:
+            collection = ModerationCollection.objects.get(id=int(collection_id))
+        except (ValueError, ModerationCollection.DoesNotExist):
+            raise Http404
+
         treenodes = self._get_selected_tree_nodes(request)
         redirect_url = self._redirect_to_changeview_url(collection_id)
 
         if request.method != 'POST':
-            context = self._custom_view_context(request)
+            context = self._custom_view_context(request, collection)
             return render(
                 request,
                 "admin/djangocms_moderation/moderationrequest/rework_confirmation.html",
                 context,
             )
         else:
-            try:
-                collection = ModerationCollection.objects.get(id=int(collection_id))
-            except (ValueError, ModerationCollection.DoesNotExist):
-                raise Http404
 
             rejected_requests = []
-
             for node in treenodes.all():
                 moderation_request = node.moderation_request
                 if moderation_request.user_can_take_moderation_action(request.user):
@@ -698,11 +718,15 @@ def rework_view(self, request):
 
     def approved_view(self, request):
         collection_id = request.GET.get('collection_id')
+        try:
+            collection = ModerationCollection.objects.get(id=int(collection_id))
+        except (ValueError, ModerationCollection.DoesNotExist):
+            raise Http404
         treenodes = self._get_selected_tree_nodes(request)
         redirect_url = self._redirect_to_changeview_url(collection_id)
 
         if request.method != 'POST':
-            context = self._custom_view_context(request)
+            context = self._custom_view_context(request, collection)
             return render(
                 request,
                 "admin/djangocms_moderation/moderationrequest/approve_confirmation.html",
@@ -723,10 +747,6 @@ def approved_view(self, request):
             and some in the second, then the reviewers we need to notify are
             different per request, depending on which stage the request is in
             """
-            try:
-                collection = ModerationCollection.objects.get(id=int(collection_id))
-            except (ValueError, ModerationCollection.DoesNotExist):
-                raise Http404
 
             approved_requests = []
             # Variable we are using to group the requests by action.step_approved
diff --git a/djangocms_moderation/admin_actions.py b/djangocms_moderation/admin_actions.py
index a8044538..0eaaa519 100644
--- a/djangocms_moderation/admin_actions.py
+++ b/djangocms_moderation/admin_actions.py
@@ -1,4 +1,5 @@
 from collections import defaultdict
+from functools import partial
 
 from django.contrib import admin
 from django.contrib.contenttypes.models import ContentType
@@ -77,7 +78,7 @@ def delete_selected(modeladmin, request, queryset):
 delete_selected.short_description = _("Remove selected")
 
 
-def publish_selected(modeladmin, request, queryset):
+def base_publish(modeladmin, request, queryset):
     if request.user != request._collection.author:
         raise PermissionDenied
 
@@ -90,8 +91,14 @@ def publish_selected(modeladmin, request, queryset):
     return HttpResponseRedirect(url)
 
 
+publish_selected = partial(base_publish)
+publish_selected.__name__ = 'publish_selected'
 publish_selected.short_description = _("Publish selected requests")
 
+unpublish_selected = partial(base_publish)
+unpublish_selected.__name__ = 'unpublish_selected'
+unpublish_selected.short_description = _("Unpublish selected requests")
+
 
 def convert_queryset_to_version_queryset(queryset):
     if not queryset:
diff --git a/djangocms_moderation/templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html b/djangocms_moderation/templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html
index c7b3122c..331a6281 100644
--- a/djangocms_moderation/templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html
+++ b/djangocms_moderation/templates/admin/djangocms_moderation/moderationrequest/publish_confirmation.html
@@ -11,7 +11,11 @@
 {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation{% endblock %}
 
 {% block content %}
+{% if collection.workflow.is_unpublishing %}
+<h3>{% trans "Are you sure you want to unpublish these items?" %}</h3>
+{% else %}
 <h3>{% trans "Are you sure you want to publish these items?" %}</h3>
+{% endif %}
 <div class="results">
     <table id="result_list">
             <thead>
diff --git a/tests/test_admin.py b/tests/test_admin.py
index 50780d9e..36de9b94 100644
--- a/tests/test_admin.py
+++ b/tests/test_admin.py
@@ -113,7 +113,7 @@ def test_publish_selected_action_visibility(self):
         # mr1 request is approved, so user1 can see the publish selected option
         self.assertIn("publish_selected", actions)
 
-        # user2 should not be able to see it
+        # user2 should not be able to see it as user2 is not the author
         mock_request.user = self.user2
         actions = self.mr_tree_admin.get_actions(request=mock_request)
         self.assertNotIn("publish_selected", actions)
@@ -124,6 +124,23 @@ def test_publish_selected_action_visibility(self):
         actions = self.mr_tree_admin.get_actions(request=mock_request)
         self.assertNotIn("publish_selected", actions)
 
+    def test_unpublish_selected_action_visibility(self):
+        self.collection.workflow.is_unpublishing = True
+        self.collection.workflow.save()
+        self.mr1.version.publish(self.user)
+        mock_request = MockRequest()
+        mock_request.user = self.user
+        mock_request._collection = self.collection
+        actions = self.mr_tree_admin.get_actions(request=mock_request)
+        # mr1 request is approved, so user1 can see the unpublish selected option
+        self.assertIn("unpublish_selected", actions)
+
+        # if there are no approved requests, user can't see the button either
+        mock_request.user = self.user
+        self.mr1.get_last_action().delete()
+        actions = self.mr_tree_admin.get_actions(request=mock_request)
+        self.assertNotIn("unpublish_selected", actions)
+
     def test_approve_and_reject_selected_action_visibility(self):
         mock_request = MockRequest()
         mock_request.user = self.user