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 %} +

{% trans "Are you sure you want to unpublish these items?" %}

+{% else %}

{% trans "Are you sure you want to publish these items?" %}

+{% endif %}
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 diff --git a/tests/test_admin_actions.py b/tests/test_admin_actions.py index e45475ce..ef21880b 100644 --- a/tests/test_admin_actions.py +++ b/tests/test_admin_actions.py @@ -9,7 +9,7 @@ from cms.test_utils.testcases import CMSTestCase from cms.test_utils.util.context_managers import signal_tester -from djangocms_versioning.constants import DRAFT, PUBLISHED +from djangocms_versioning.constants import DRAFT, PUBLISHED, UNPUBLISHED from djangocms_versioning.models import Version from djangocms_moderation import constants @@ -551,6 +551,84 @@ def test_view_doesnt_reject_when_user_cant_take_moderation_action( self.assertFalse(notify_author_mock.called) +class UnpublishSelectedTest(CMSTestCase): + def setUp(self): + self.user = factories.UserFactory(is_staff=True, is_superuser=True) + + # TODO approving all MR will put the collection in archived status. + # So that is the current assumption that it should be correct. + self.collection = factories.ModerationCollectionFactory( + author=self.user, status=constants.ARCHIVED + ) + self.collection.workflow.is_unpublishing = True + self.collection.workflow.save() + self.role1 = Role.objects.create(name="Role 1", user=self.user) + self.role2 = Role.objects.create(name="Role 2", user=factories.UserFactory(is_staff=True, is_superuser=True)) + self.collection.workflow.steps.create(role=self.role1, is_required=True, order=1) + self.collection.workflow.steps.create(role=self.role2, is_required=True, order=1) + + self.mr1 = factories.ModerationRequestFactory( + id=1, collection=self.collection + ) + self.mr2 = factories.ModerationRequestFactory( + id=2, collection=self.collection + ) + self.root1 = factories.RootModerationRequestTreeNodeFactory( + id=4, moderation_request=self.mr1 + ) + factories.ChildModerationRequestTreeNodeFactory( + id=5, moderation_request=self.mr2, parent=self.root1 + ) + self.root2 = factories.RootModerationRequestTreeNodeFactory( + id=6, moderation_request=self.mr2 + ) + + # Request 1 is approved, request 2 is started + self.mr1.actions.create(by_user=self.user, action=constants.ACTION_STARTED) + self.mr2.actions.create(by_user=self.user, action=constants.ACTION_STARTED) + self.mr1.update_status(constants.ACTION_APPROVED, self.role1.user) + self.mr1.update_status(constants.ACTION_APPROVED, self.role2.user) + + self.mr1.version.publish(self.user) + self.mr2.version.publish(self.user) + + # we need to refresh the db so that version state is reflected correctly + self.mr1.refresh_from_db() + self.mr2.refresh_from_db() + + # Login as the collection author + self.client.force_login(self.user) + + self.url = reverse("admin:djangocms_moderation_moderationrequesttreenode_changelist") + self.url += "?moderation_request__collection__id={}".format(self.collection.pk) + + @mock.patch("django.contrib.messages.success") + def test_unpublish_selected_unpublishes_approved_request(self, message_mock): + + # resp = self.client.get(self.url) + # import pdb; pdb.set_trace() + + print('correct flow') + data = get_url_data(self, "unpublish_selected") + response = self.client.post(self.url, data) + # And now go to the view the action redirects to + response = self.client.post(response.url) + + # Response is correct + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, self.url) + self.assertEqual(message_mock.call_args[0][1], '1 request successfully unpublished') + + version1 = Version.objects.get(pk=self.mr1.version.pk) + version2 = Version.objects.get(pk=self.mr2.version.pk) + self.mr1.refresh_from_db() + self.mr2.refresh_from_db() + self.assertEqual(version1.state, UNPUBLISHED) + self.assertEqual(version2.state, PUBLISHED) + self.assertFalse(self.mr1.is_active) + self.assertTrue(self.mr2.is_active) + + class PublishSelectedTest(CMSTestCase): def setUp(self): @@ -647,6 +725,7 @@ def test_signal_when_published(self): self.assertEquals(published_mr[0], self.moderation_request1) self.assertEquals(kwargs['workflow'], self.collection.workflow) + # TODO Andrew, how is it intended that collection status should change? @unittest.skip("Skip until collection status bugs fixed") @mock.patch("django.contrib.messages.success") def test_publish_selected_sets_collection_to_archived_if_all_requests_published(self, messages_mock):