diff --git a/HISTORY.rst b/HISTORY.rst index 69dbd99bb5..211deba11d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,9 @@ * Add missing recursive option to Docker entrypoint chown. GitLab issue #668. Thanks to John Wice (@brilthor) for the report. +* Add support for deleting the parsed content of a document + of selection of documents. +* Add parsed content deleted event. 3.2.7 (2019-08-28) ================== diff --git a/docs/releases/3.2.8.rst b/docs/releases/3.2.8.rst index 4e131dea6f..91d8d864b3 100644 --- a/docs/releases/3.2.8.rst +++ b/docs/releases/3.2.8.rst @@ -27,6 +27,10 @@ Other changes - Add missing recursive option to Docker entrypoint chown. GitLab issue #668. Thanks to John Wice (@brilthor) for the report. +- Add support for deleting the parsed content of a document + of selection of documents. +- Add parsed content deleted event. + Removals -------- diff --git a/mayan/apps/document_parsing/apps.py b/mayan/apps/document_parsing/apps.py index 844198cd9d..f481252e8b 100644 --- a/mayan/apps/document_parsing/apps.py +++ b/mayan/apps/document_parsing/apps.py @@ -14,15 +14,22 @@ from mayan.apps.common.menus import ( ) from mayan.apps.documents.search import document_search, document_page_search from mayan.apps.documents.signals import post_version_upload +from mayan.apps.events.classes import ModelEventType from mayan.apps.navigation.classes import SourceColumn from .dependencies import * # NOQA +from .events import ( + event_parsing_document_content_deleted, + event_parsing_document_version_submit, + event_parsing_document_version_finish +) from .handlers import ( handler_index_document, handler_initialize_new_parsing_settings, handler_parse_document_version ) from .links import ( - link_document_content, link_document_page_content, + link_document_content, link_document_content_delete, + link_document_content_delete_multiple, link_document_page_content, link_document_content_download, link_document_parsing_errors_list, link_document_submit_multiple, link_document_submit, link_document_type_parsing_settings, link_document_type_submit, @@ -85,6 +92,14 @@ class DocumentParsingApp(MayanAppConfig): value=method_document_version_parsing_submit ) + ModelEventType.register( + model=Document, event_types=( + event_parsing_document_content_deleted, + event_parsing_document_version_submit, + event_parsing_document_version_finish + ) + ) + ModelField( model=Document, name='versions__pages__content__content' ) @@ -136,16 +151,21 @@ class DocumentParsingApp(MayanAppConfig): sources=(DocumentType,) ) menu_multi_item.bind_links( - links=(link_document_submit_multiple,), sources=(Document,) + links=( + link_document_content_delete_multiple, + link_document_submit_multiple, + ), sources=(Document,) ) menu_secondary.bind_links( links=( + link_document_content_delete, link_document_content_download, link_document_parsing_errors_list, link_document_submit ), sources=( 'document_parsing:document_content', + 'document_parsing:document_content_delete', 'document_parsing:document_content_download', 'document_parsing:document_parsing_error_list', 'document_parsing:document_submit', diff --git a/mayan/apps/document_parsing/events.py b/mayan/apps/document_parsing/events.py index 07f4a82b2a..29f9da85ee 100644 --- a/mayan/apps/document_parsing/events.py +++ b/mayan/apps/document_parsing/events.py @@ -8,6 +8,10 @@ namespace = EventTypeNamespace( label=_('Document parsing'), name='document_parsing' ) +event_parsing_document_content_deleted = namespace.add_event_type( + label=_('Document parsed content deleted'), + name='document_content_deleted' +) event_parsing_document_version_submit = namespace.add_event_type( label=_('Document version submitted for parsing'), name='version_submit' ) diff --git a/mayan/apps/document_parsing/icons.py b/mayan/apps/document_parsing/icons.py index 77f75f40db..6aad4a8ea2 100644 --- a/mayan/apps/document_parsing/icons.py +++ b/mayan/apps/document_parsing/icons.py @@ -3,6 +3,11 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon icon_document_content = Icon(driver_name='fontawesome', symbol='font') +icon_document_content_delete = Icon( + driver_name='fontawesome-dual', + primary_symbol='font', + secondary_symbol='minus' +) icon_document_parsing_errors_list = Icon( driver_name='fontawesome', symbol='font' ) diff --git a/mayan/apps/document_parsing/links.py b/mayan/apps/document_parsing/links.py index b98237ca52..4fc81bca4d 100644 --- a/mayan/apps/document_parsing/links.py +++ b/mayan/apps/document_parsing/links.py @@ -16,6 +16,17 @@ link_document_content = Link( permissions=(permission_content_view,), text=_('Content'), view='document_parsing:document_content' ) +link_document_content_delete = Link( + args='resolved_object.id', + icon_class_path='mayan.apps.document_parsing.icons.icon_document_content_delete', + permissions=(permission_parse_document,), text=_('Delete parsed content'), + view='document_parsing:document_content_delete', +) +link_document_content_delete_multiple = Link( + icon_class_path='mayan.apps.document_parsing.icons.icon_document_content_delete', + text=_('Delete parsed content'), + view='document_parsing:document_content_delete_multiple', +) link_document_page_content = Link( args='resolved_object.id', icon_class_path='mayan.apps.document_parsing.icons.icon_document_content', diff --git a/mayan/apps/document_parsing/managers.py b/mayan/apps/document_parsing/managers.py index c9a70e6ee9..1669f89e70 100644 --- a/mayan/apps/document_parsing/managers.py +++ b/mayan/apps/document_parsing/managers.py @@ -6,9 +6,12 @@ import traceback from django.apps import apps from django.conf import settings -from django.db import models +from django.db import models, transaction -from .events import event_parsing_document_version_finish +from .events import ( + event_parsing_document_content_deleted, + event_parsing_document_version_finish +) from .parsers import Parser from .signals import post_document_version_parsing @@ -16,6 +19,15 @@ logger = logging.getLogger(__name__) class DocumentPageContentManager(models.Manager): + def delete_content_for(self, document, user=None): + with transaction.atomic(): + for document_page in document.pages.all(): + self.filter(document_page=document_page).delete() + + event_parsing_document_content_deleted.commit( + actor=user, target=document + ) + def process_document_version(self, document_version): logger.info( 'Starting parsing for document version: %s', document_version diff --git a/mayan/apps/document_parsing/tests/test_events.py b/mayan/apps/document_parsing/tests/test_events.py index 273e492cde..1a2860305d 100644 --- a/mayan/apps/document_parsing/tests/test_events.py +++ b/mayan/apps/document_parsing/tests/test_events.py @@ -6,15 +6,33 @@ from mayan.apps.documents.tests.literals import TEST_PDF_DOCUMENT_FILENAME from mayan.apps.documents.tests.test_models import GenericDocumentTestCase from ..events import ( + event_parsing_document_content_deleted, event_parsing_document_version_submit, event_parsing_document_version_finish ) +from ..models import DocumentPageContent class DocumentParsingEventsTestCase(GenericDocumentTestCase): # Ensure we use a PDF file test_document_filename = TEST_PDF_DOCUMENT_FILENAME + def test_document_content_deleted_event(self): + Action.objects.all().delete() + DocumentPageContent.objects.delete_content_for( + document=self.test_document + ) + + # Get the oldest action + action = Action.objects.order_by('-timestamp').last() + + self.assertEqual( + action.target, self.test_document + ) + self.assertEqual( + action.verb, event_parsing_document_content_deleted.id + ) + def test_document_version_submit_event(self): Action.objects.all().delete() self.test_document.submit_for_parsing() diff --git a/mayan/apps/document_parsing/tests/test_views.py b/mayan/apps/document_parsing/tests/test_views.py index 518a15423e..e7792dfd70 100644 --- a/mayan/apps/document_parsing/tests/test_views.py +++ b/mayan/apps/document_parsing/tests/test_views.py @@ -6,6 +6,7 @@ from mayan.apps.documents.tests import ( GenericDocumentViewTestCase, TEST_HYBRID_DOCUMENT ) +from ..models import DocumentPageContent from ..permissions import ( permission_content_view, permission_document_type_parsing_setup, permission_parse_document @@ -15,22 +16,46 @@ from ..utils import get_document_content from .literals import TEST_DOCUMENT_CONTENT -@override_settings(DOCUMENT_PARSING_AUTO_PARSING=True) -class DocumentContentViewsTestCase(GenericDocumentViewTestCase): - _skip_file_descriptor_test = True +class DocumentContentViewTestMixin(object): + def _request_test_document_content_delete_view(self): + return self.post( + viewname='document_parsing:document_content_delete', kwargs={ + 'pk': self.test_document.pk + } + ) - # Ensure we use a PDF file - test_document_filename = TEST_HYBRID_DOCUMENT + def _request_test_document_content_download_view(self): + return self.get( + viewname='document_parsing:document_content_download', + kwargs={'pk': self.test_document.pk} + ) - def _request_document_content_view(self): + def _request_test_document_content_view(self): return self.get( 'document_parsing:document_content', kwargs={ 'pk': self.test_document.pk } ) + def _request_test_document_page_content_view(self): + return self.get( + viewname='document_parsing:document_page_content', kwargs={ + 'pk': self.test_document.pages.first().pk, + } + ) + + +@override_settings(DOCUMENT_PARSING_AUTO_PARSING=True) +class DocumentContentViewsTestCase( + DocumentContentViewTestMixin, GenericDocumentViewTestCase +): + _skip_file_descriptor_test = True + + # Ensure we use a PDF file + test_document_filename = TEST_HYBRID_DOCUMENT + def test_document_content_view_no_permissions(self): - response = self._request_document_content_view() + response = self._request_test_document_content_view() self.assertEqual(response.status_code, 404) def test_document_content_view_with_access(self): @@ -38,20 +63,37 @@ class DocumentContentViewsTestCase(GenericDocumentViewTestCase): obj=self.test_document, permission=permission_content_view ) - response = self._request_document_content_view() + response = self._request_test_document_content_view() self.assertContains( response=response, text=TEST_DOCUMENT_CONTENT, status_code=200 ) - def _request_document_page_content_view(self): - return self.get( - viewname='document_parsing:document_page_content', kwargs={ - 'pk': self.test_document.pages.first().pk, - } + def test_document_content_delete_view_no_permissions(self): + response = self._request_test_document_content_delete_view() + self.assertEqual(response.status_code, 404) + + self.assertTrue( + DocumentPageContent.objects.filter( + document_page=self.test_document.pages.first() + ).exists() + ) + + def test_document_content_delete_view_with_access(self): + self.grant_access( + obj=self.test_document, permission=permission_parse_document + ) + + response = self._request_test_document_content_delete_view() + self.assertEqual(response.status_code, 302) + + self.assertFalse( + DocumentPageContent.objects.filter( + document_page=self.test_document.pages.first() + ).exists() ) def test_document_page_content_view_no_permissions(self): - response = self._request_document_page_content_view() + response = self._request_test_document_page_content_view() self.assertEqual(response.status_code, 404) def test_document_page_content_view_with_access(self): @@ -59,19 +101,13 @@ class DocumentContentViewsTestCase(GenericDocumentViewTestCase): permission=permission_content_view, obj=self.test_document ) - response = self._request_document_page_content_view() + response = self._request_test_document_page_content_view() self.assertContains( response=response, text=TEST_DOCUMENT_CONTENT, status_code=200 ) - def _request_document_content_download_view(self): - return self.get( - viewname='document_parsing:document_content_download', - kwargs={'pk': self.test_document.pk} - ) - def test_document_parsing_download_view_no_permission(self): - response = self._request_document_content_download_view() + response = self._request_test_document_content_download_view() self.assertEqual(response.status_code, 403) def test_download_view_with_access(self): @@ -80,7 +116,7 @@ class DocumentContentViewsTestCase(GenericDocumentViewTestCase): permission=permission_content_view, obj=self.test_document ) - response = self._request_document_content_download_view() + response = self._request_test_document_content_download_view() self.assertEqual(response.status_code, 200) self.assert_download_response( @@ -89,12 +125,20 @@ class DocumentContentViewsTestCase(GenericDocumentViewTestCase): ), ) + +class DocumentTypeContentViewsTestMixin(object): def _request_test_document_type_parsing_settings(self): return self.get( viewname='document_parsing:document_type_parsing_settings', - kwargs={'pk': self.test_document.document_type.pk} + kwargs={'pk': self.test_document_type.pk} ) + +class DocumentTypeContentViewsTestCase( + DocumentTypeContentViewsTestMixin, GenericDocumentViewTestCase +): + auto_upload_document = False + def test_document_type_parsing_settings_view_no_permission(self): response = self._request_test_document_type_parsing_settings() self.assertEqual(response.status_code, 404) @@ -102,7 +146,7 @@ class DocumentContentViewsTestCase(GenericDocumentViewTestCase): def test_document_type_parsing_settings_view_with_access(self): self.grant_access( permission=permission_document_type_parsing_setup, - obj=self.test_document.document_type + obj=self.test_document_type ) response = self._request_test_document_type_parsing_settings() diff --git a/mayan/apps/document_parsing/urls.py b/mayan/apps/document_parsing/urls.py index 20de63eefd..f590236176 100644 --- a/mayan/apps/document_parsing/urls.py +++ b/mayan/apps/document_parsing/urls.py @@ -4,10 +4,10 @@ from django.conf.urls import url from .api_views import APIDocumentPageContentView from .views import ( - DocumentContentView, DocumentContentDownloadView, - DocumentPageContentView, DocumentParsingErrorsListView, - DocumentSubmitView, DocumentTypeSettingsEditView, DocumentTypeSubmitView, - ParseErrorListView + DocumentContentView, DocumentContentDeleteView, + DocumentContentDownloadView, DocumentPageContentView, + DocumentParsingErrorsListView, DocumentSubmitView, + DocumentTypeSettingsEditView, DocumentTypeSubmitView, ParseErrorListView ) urlpatterns = [ @@ -16,21 +16,23 @@ urlpatterns = [ view=DocumentContentView.as_view(), name='document_content' ), url( - regex=r'^documents/pages/(?P\d+)/content/$', - view=DocumentPageContentView.as_view(), name='document_page_content' + regex=r'^documents/(?P\d+)/content/delete/$', + view=DocumentContentDeleteView.as_view(), + name='document_content_delete' + ), + url( + regex=r'^documents/multiple/content/delete/$', + view=DocumentContentDeleteView.as_view(), + name='document_content_delete_multiple' ), url( regex=r'^documents/(?P\d+)/content/download/$', - view=DocumentContentDownloadView.as_view(), name='document_content_download' + view=DocumentContentDownloadView.as_view(), + name='document_content_download' ), url( - regex=r'^document_types/submit/$', - view=DocumentTypeSubmitView.as_view(), name='document_type_submit' - ), - url( - regex=r'^document_types/(?P\d+)/parsing/settings/$', - view=DocumentTypeSettingsEditView.as_view(), - name='document_type_parsing_settings' + regex=r'^documents/pages/(?P\d+)/content/$', + view=DocumentPageContentView.as_view(), name='document_page_content' ), url( regex=r'^documents/(?P\d+)/submit/$', @@ -45,6 +47,15 @@ urlpatterns = [ view=DocumentParsingErrorsListView.as_view(), name='document_parsing_error_list' ), + url( + regex=r'^document_types/submit/$', + view=DocumentTypeSubmitView.as_view(), name='document_type_submit' + ), + url( + regex=r'^document_types/(?P\d+)/parsing/settings/$', + view=DocumentTypeSettingsEditView.as_view(), + name='document_type_parsing_settings' + ), url( regex=r'^errors/all/$', view=ParseErrorListView.as_view(), name='error_list' diff --git a/mayan/apps/document_parsing/views.py b/mayan/apps/document_parsing/views.py index 5256fc7c6d..de9cb3ef0b 100644 --- a/mayan/apps/document_parsing/views.py +++ b/mayan/apps/document_parsing/views.py @@ -15,7 +15,7 @@ from mayan.apps.documents.forms import DocumentTypeFilteredSelectForm from mayan.apps.documents.models import Document, DocumentPage, DocumentType from .forms import DocumentContentForm, DocumentPageContentForm -from .models import DocumentVersionParseError +from .models import DocumentPageContent, DocumentVersionParseError from .permissions import ( permission_content_view, permission_document_type_parsing_setup, permission_parse_document @@ -23,6 +23,34 @@ from .permissions import ( from .utils import get_document_content +class DocumentContentDeleteView(MultipleObjectConfirmActionView): + model = Document + object_permission = permission_parse_document + success_message = 'Deleted parsed content of %(count)d document.' + success_message_plural = 'Deleted parsed content of %(count)d documents.' + + def get_extra_context(self): + queryset = self.object_list + + result = { + 'title': ungettext( + singular='Delete the parsed content of the selected document?', + plural='Delete the parsed content of the selected documents?', + number=queryset.count() + ) + } + + if queryset.count() == 1: + result['object'] = queryset.first() + + return result + + def object_action(self, form, instance): + DocumentPageContent.objects.delete_content_for( + document=instance, user=self.request.user + ) + + class DocumentContentView(SingleObjectDetailView): form_class = DocumentContentForm model = Document