diff --git a/HISTORY.rst b/HISTORY.rst index fbcb614064..54f47a9be5 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -131,6 +131,8 @@ - Add the document template sandbox feature. - Use the generic TemplateField for the expression field of index tree templates. +- Add document trashed event. Closes GitLab issue #608 + Thanks to Vikas Kedia (@vikaskedia) for the report. 3.2.10 (2019-XX-XX) =================== diff --git a/docs/releases/3.3.rst b/docs/releases/3.3.rst index ddecc16450..e138330134 100644 --- a/docs/releases/3.3.rst +++ b/docs/releases/3.3.rst @@ -216,6 +216,7 @@ Bugs fixed or issues closed - :gitlab-issue:`532` Workflow preview isn't updated right after transitions are modified - :gitlab-issue:`540` hint-outdated/update documentation - :gitlab-issue:`594` 3.2b1: Unable to install/run under Python 3.5/3.6/3.7 +- :gitlab-issue:`608` How to know who put a document in trash can? [Video] - :gitlab-issue:`634` Failing docker entrypoint when using secret config - :gitlab-issue:`635` Build a docker image for Python3 - :gitlab-issue:`640` UX: "Toast" Popup position prevents access to actions diff --git a/mayan/apps/documents/events.py b/mayan/apps/documents/events.py index 77a7b8136e..bfecda5c7d 100644 --- a/mayan/apps/documents/events.py +++ b/mayan/apps/documents/events.py @@ -18,6 +18,9 @@ event_document_new_version = namespace.add_event_type( event_document_properties_edit = namespace.add_event_type( label=_('Document properties edited'), name='document_edit' ) +event_document_trashed = namespace.add_event_type( + label=_('Document trashed'), name='document_trashed' +) # The type of an existing document is changed to another type event_document_type_change = namespace.add_event_type( label=_('Document type changed'), name='document_type_change' diff --git a/mayan/apps/documents/models/document_models.py b/mayan/apps/documents/models/document_models.py index b6de2bee45..6a50dcbcf8 100644 --- a/mayan/apps/documents/models/document_models.py +++ b/mayan/apps/documents/models/document_models.py @@ -5,7 +5,7 @@ import uuid from django.apps import apps from django.core.files import File -from django.db import models +from django.db import models, transaction from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now @@ -13,7 +13,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _ from ..events import ( event_document_create, event_document_properties_edit, - event_document_type_change, + event_document_trashed, event_document_type_change, ) from ..managers import DocumentManager, PassthroughManager, TrashCanManager from ..settings import setting_language @@ -112,11 +112,14 @@ class Document(models.Model): def delete(self, *args, **kwargs): to_trash = kwargs.pop('to_trash', True) + _user = kwargs.pop('_user', True) if not self.in_trash and to_trash: self.in_trash = True self.deleted_date_time = now() - self.save() + with transaction.atomic(): + self.save(_commit_events=False) + event_document_trashed.commit(actor=_user, target=self) else: for version in self.versions.all(): version.delete() diff --git a/mayan/apps/documents/tests/mixins.py b/mayan/apps/documents/tests/mixins.py index 86ddba9bc6..dc4028a910 100644 --- a/mayan/apps/documents/tests/mixins.py +++ b/mayan/apps/documents/tests/mixins.py @@ -268,3 +268,50 @@ class DocumentViewTestMixin(object): def _request_empty_trash_view(self): return self.post(viewname='documents:trash_can_empty') + + +class TrashedDocumentViewTestMixin(object): + def _request_document_trash_get_view(self): + return self.get( + viewname='documents:document_trash', kwargs={ + 'pk': self.test_document.pk + } + ) + + def _request_document_trash_post_view(self): + return self.post( + viewname='documents:document_trash', kwargs={ + 'pk': self.test_document.pk + } + ) + + def _request_trashed_document_restore_get_view(self): + return self.get( + viewname='documents:document_restore', kwargs={ + 'pk': self.test_document.pk + } + ) + + def _request_trashed_document_restore_post_view(self): + return self.post( + viewname='documents:document_restore', kwargs={ + 'pk': self.test_document.pk + } + ) + + def _request_trashed_document_delete_get_view(self): + return self.get( + viewname='documents:document_delete', kwargs={ + 'pk': self.test_document.pk + } + ) + + def _request_trashed_document_delete_post_view(self): + return self.post( + viewname='documents:document_delete', kwargs={ + 'pk': self.test_document.pk + } + ) + + def _request_trashed_document_list_view(self): + return self.get(viewname='documents:document_list_deleted') diff --git a/mayan/apps/documents/tests/test_events.py b/mayan/apps/documents/tests/test_events.py index cc18d46981..f2e87dd56a 100644 --- a/mayan/apps/documents/tests/test_events.py +++ b/mayan/apps/documents/tests/test_events.py @@ -3,13 +3,16 @@ from __future__ import unicode_literals from actstream.models import Action from django_downloadview import assert_download_response -from ..events import event_document_download, event_document_view +from ..events import ( + event_document_download, event_document_trashed, event_document_view +) from ..permissions import ( - permission_document_download, permission_document_view + permission_document_download, permission_document_trash, + permission_document_view ) from .base import GenericDocumentViewTestCase - +from .mixins import TrashedDocumentViewTestMixin TEST_DOCUMENT_TYPE_EDITED_LABEL = 'test document type edited label' TEST_DOCUMENT_TYPE_2_LABEL = 'test document type 2 label' @@ -32,21 +35,22 @@ class DocumentEventsTestMixin(object): class DocumentEventsTestCase( - DocumentEventsTestMixin, GenericDocumentViewTestCase + DocumentEventsTestMixin, TrashedDocumentViewTestMixin, + GenericDocumentViewTestCase ): - def test_document_download_event_no_permissions(self): + def setUp(self): + super(DocumentEventsTestCase, self).setUp() Action.objects.all().delete() + def test_document_download_event_no_permission(self): response = self._request_test_document_download_view() self.assertEqual(response.status_code, 403) self.assertEqual(list(Action.objects.any(obj=self.test_document)), []) - def test_document_download_event_with_permissions(self): + def test_document_download_event_with_access(self): self.expected_content_types = ('image/png; charset=utf-8',) - Action.objects.all().delete() - self.grant_access( obj=self.test_document, permission=permission_document_download ) @@ -66,17 +70,13 @@ class DocumentEventsTestCase( self.assertEqual(event.target, self.test_document) self.assertEqual(event.verb, event_document_download.id) - def test_document_view_event_no_permissions(self): - Action.objects.all().delete() - + def test_document_view_event_no_permission(self): response = self._request_test_document_preview_view() self.assertEqual(response.status_code, 404) self.assertEqual(list(Action.objects.any(obj=self.test_document)), []) - def test_document_view_event_with_permissions(self): - Action.objects.all().delete() - + def test_document_view_event_with_access(self): self.grant_access( obj=self.test_document, permission=permission_document_view ) @@ -88,3 +88,22 @@ class DocumentEventsTestCase( self.assertEqual(event.actor, self._test_case_user) self.assertEqual(event.target, self.test_document) self.assertEqual(event.verb, event_document_view.id) + + def test_document_trashed_view_event_no_permission(self): + response = self._request_document_trash_post_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual(list(Action.objects.any(obj=self.test_document)), []) + + def test_document_trashed_view_event_with_access(self): + self.grant_access( + obj=self.test_document, permission=permission_document_trash + ) + + response = self._request_document_trash_post_view() + self.assertEqual(response.status_code, 302) + + event = Action.objects.any(obj=self.test_document).first() + self.assertEqual(event.actor, self._test_case_user) + self.assertEqual(event.target, self.test_document) + self.assertEqual(event.verb, event_document_trashed.id) diff --git a/mayan/apps/documents/tests/test_trashed_document_views.py b/mayan/apps/documents/tests/test_trashed_document_views.py index 9b2f4c7c5e..858b8f3fed 100644 --- a/mayan/apps/documents/tests/test_trashed_document_views.py +++ b/mayan/apps/documents/tests/test_trashed_document_views.py @@ -7,57 +7,11 @@ from ..permissions import ( ) from .base import GenericDocumentViewTestCase +from .mixins import TrashedDocumentViewTestMixin -class TrashedDocumentTestMixin(object): - def _request_document_trash_get_view(self): - return self.get( - viewname='documents:document_trash', kwargs={ - 'pk': self.test_document.pk - } - ) - - def _request_document_trash_post_view(self): - return self.post( - viewname='documents:document_trash', kwargs={ - 'pk': self.test_document.pk - } - ) - - def _request_trashed_document_restore_get_view(self): - return self.get( - viewname='documents:document_restore', kwargs={ - 'pk': self.test_document.pk - } - ) - - def _request_trashed_document_restore_post_view(self): - return self.post( - viewname='documents:document_restore', kwargs={ - 'pk': self.test_document.pk - } - ) - - def _request_trashed_document_delete_get_view(self): - return self.get( - viewname='documents:document_delete', kwargs={ - 'pk': self.test_document.pk - } - ) - - def _request_trashed_document_delete_post_view(self): - return self.post( - viewname='documents:document_delete', kwargs={ - 'pk': self.test_document.pk - } - ) - - def _request_trashed_document_list_view(self): - return self.get(viewname='documents:document_list_deleted') - - -class TrashedDocumentTestCase( - TrashedDocumentTestMixin, GenericDocumentViewTestCase +class TrashedDocumentViewTestCase( + TrashedDocumentViewTestMixin, GenericDocumentViewTestCase ): def test_document_trash_get_view_no_permissions(self): document_count = Document.objects.count() diff --git a/mayan/apps/documents/views/trashed_document_views.py b/mayan/apps/documents/views/trashed_document_views.py index bf783303ed..d55e99e05e 100644 --- a/mayan/apps/documents/views/trashed_document_views.py +++ b/mayan/apps/documents/views/trashed_document_views.py @@ -56,7 +56,7 @@ class DocumentTrashView(MultipleObjectConfirmActionView): return result def object_action(self, form, instance): - instance.delete() + instance.delete(_user=self.request.user) class EmptyTrashCanView(ConfirmView):