diff --git a/HISTORY.rst b/HISTORY.rst index a71d1f4e8a..78904f480b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -68,6 +68,7 @@ as a tooltip. - Update numeric dashboard widget to display thousand commas. +- Add support for disabling document pages. 3.2.6 (2019-07-10) ================== diff --git a/docs/releases/3.3.rst b/docs/releases/3.3.rst index a1d56f4807..632d7d6aa5 100644 --- a/docs/releases/3.3.rst +++ b/docs/releases/3.3.rst @@ -83,6 +83,7 @@ Changes as a tooltip. - Update numeric dashboard widget to display thousand commas. +- Add support for disabling document pages. Removals -------- diff --git a/mayan/apps/document_parsing/apps.py b/mayan/apps/document_parsing/apps.py index 844198cd9d..8bffb20ead 100644 --- a/mayan/apps/document_parsing/apps.py +++ b/mayan/apps/document_parsing/apps.py @@ -86,7 +86,7 @@ class DocumentParsingApp(MayanAppConfig): ) ModelField( - model=Document, name='versions__pages__content__content' + model=Document, name='versions__version_pages__content__content' ) ModelPermission.register( @@ -118,7 +118,7 @@ class DocumentParsingApp(MayanAppConfig): ) document_search.add_model_field( - field='versions__pages__content__content', label=_('Content') + field='versions__version_pages__content__content', label=_('Content') ) document_page_search.add_model_field( diff --git a/mayan/apps/documents/api_views.py b/mayan/apps/documents/api_views.py index 443a1a38b6..26878c8acb 100644 --- a/mayan/apps/documents/api_views.py +++ b/mayan/apps/documents/api_views.py @@ -174,7 +174,7 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): ) def get_queryset(self): - return self.get_document_version().pages.all() + return self.get_document_version().pages_all.all() def get_serializer(self, *args, **kwargs): return None diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index aec4a06e21..020e2c7a04 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -60,6 +60,8 @@ from .links import ( link_document_multiple_download, link_document_multiple_favorites_add, link_document_multiple_favorites_remove, link_document_multiple_restore, link_document_multiple_trash, link_document_multiple_update_page_count, + link_document_page_disable, link_document_page_multiple_disable, + link_document_page_enable, link_document_page_multiple_enable, link_document_page_navigation_first, link_document_page_navigation_last, link_document_page_navigation_next, link_document_page_navigation_previous, link_document_page_return, link_document_page_rotate_left, @@ -214,12 +216,21 @@ class DocumentsApp(MayanAppConfig): ModelPermission.register_inheritance( model=Document, related='document_type', ) + ModelPermission.register_manager( + model=Document, manager_name='passthrough' + ) ModelPermission.register_inheritance( model=DocumentPage, related='document_version__document', ) + ModelPermission.register_manager( + model=DocumentPage, manager_name='passthrough' + ) ModelPermission.register_inheritance( model=DocumentPageResult, related='document_version__document', ) + ModelPermission.register_manager( + model=DocumentPageResult, manager_name='passthrough' + ) ModelPermission.register_inheritance( model=DocumentTypeFilename, related='document_type', ) @@ -269,6 +280,13 @@ class DocumentsApp(MayanAppConfig): instance=context['object'] ), label=_('Thumbnail'), source=DocumentPage ) + SourceColumn( + attribute='enabled', include_label=True, source=DocumentPage, + widget=TwoStateWidget + ) + SourceColumn( + attribute='page_number', include_label=True, source=DocumentPage + ) SourceColumn( attribute='get_label', is_identifier=True, @@ -503,6 +521,16 @@ class DocumentsApp(MayanAppConfig): link_document_page_navigation_last ), sources=(DocumentPage,) ) + menu_multi_item.bind_links( + links=( + link_document_page_multiple_disable, + link_document_page_multiple_enable + ), sources=(DocumentPage,) + ) + menu_object.bind_links( + links=(link_document_page_disable, link_document_page_enable), + sources=(DocumentPage,) + ) menu_list_facet.bind_links( links=(link_transformation_list,), sources=(DocumentPage,) ) diff --git a/mayan/apps/documents/icons.py b/mayan/apps/documents/icons.py index fe745fe538..fb891fd585 100644 --- a/mayan/apps/documents/icons.py +++ b/mayan/apps/documents/icons.py @@ -25,8 +25,6 @@ icon_dashboard_new_documents_this_month = Icon( icon_dashboard_total_document = Icon( driver_name='fontawesome', symbol='book' ) - - icon_document_quick_download = Icon( driver_name='fontawesome', symbol='download' ) @@ -104,6 +102,14 @@ icon_favorite_document_remove = Icon( secondary_symbol='minus' ) +# Document pages + +icon_document_page_disable = Icon( + driver_name='fontawesomecss', css_classes='far fa-eye-slash' +) +icon_document_page_enable = Icon( + driver_name='fontawesomecss', css_classes='far fa-eye' +) icon_document_page_navigation_first = Icon( driver_name='fontawesome', symbol='step-backward' ) diff --git a/mayan/apps/documents/links.py b/mayan/apps/documents/links.py index 17ed67313e..94463767f7 100644 --- a/mayan/apps/documents/links.py +++ b/mayan/apps/documents/links.py @@ -19,14 +19,14 @@ from .icons import ( icon_duplicated_document_list, icon_duplicated_document_scan ) from .permissions import ( - permission_document_delete, permission_document_download, - permission_document_properties_edit, permission_document_print, - permission_document_restore, permission_document_tools, - permission_document_version_revert, permission_document_view, - permission_document_trash, permission_document_type_create, - permission_document_type_delete, permission_document_type_edit, - permission_document_type_view, permission_empty_trash, - permission_document_version_view + permission_document_delete, permission_document_edit, + permission_document_download, permission_document_properties_edit, + permission_document_print, permission_document_restore, + permission_document_tools, permission_document_version_revert, + permission_document_view, permission_document_trash, + permission_document_type_create, permission_document_type_delete, + permission_document_type_edit, permission_document_type_view, + permission_empty_trash, permission_document_version_view ) from .settings import setting_zoom_max_level, setting_zoom_min_level @@ -270,6 +270,29 @@ link_trash_can_empty = Link( ) # Document pages + +link_document_page_disable = Link( + icon_class_path='mayan.apps.documents.icons.icon_document_page_disable', + kwargs={'pk': 'resolved_object.id'}, + permissions=(permission_document_edit,), text=_('Disable page'), + view='documents:document_page_disable' +) +link_document_page_multiple_disable = Link( + icon_class_path='mayan.apps.documents.icons.icon_document_page_disable', + text=_('Disable pages'), + view='documents:document_page_multiple_disable' +) +link_document_page_enable = Link( + icon_class_path='mayan.apps.documents.icons.icon_document_page_enable', + kwargs={'pk': 'resolved_object.id'}, + permissions=(permission_document_edit,), text=_('Enable page'), + view='documents:document_page_enable' +) +link_document_page_multiple_enable = Link( + icon_class_path='mayan.apps.documents.icons.icon_document_page_enable', + text=_('Enable pages'), + view='documents:document_page_multiple_enable' +) link_document_page_navigation_first = Link( args='resolved_object.pk', conditional_disable=is_first_page, icon_class=icon_document_page_navigation_first, diff --git a/mayan/apps/documents/managers.py b/mayan/apps/documents/managers.py index 599c4b8d88..6c9dd92640 100644 --- a/mayan/apps/documents/managers.py +++ b/mayan/apps/documents/managers.py @@ -22,7 +22,7 @@ class DocumentManager(models.Manager): def get_queryset(self): return TrashCanQuerySet( - self.model, using=self._db + model=self.model, using=self._db ).filter(in_trash=False).filter(is_stub=False) @@ -38,6 +38,11 @@ class DocumentPageManager(models.Manager): return self.get(document_version__pk=document_version.pk, page_number=page_number) + def get_queryset(self): + return models.QuerySet( + model=self.model, using=self._db + ).filter(enabled=True) + class DocumentTypeManager(models.Manager): def check_delete_periods(self): diff --git a/mayan/apps/documents/migrations/0051_documentpage_enabled.py b/mayan/apps/documents/migrations/0051_documentpage_enabled.py new file mode 100644 index 0000000000..0c74c9cc00 --- /dev/null +++ b/mayan/apps/documents/migrations/0051_documentpage_enabled.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-07-29 07:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0050_auto_20190725_0451'), + ] + + operations = [ + migrations.AddField( + model_name='documentpage', + name='enabled', + field=models.BooleanField(default=True, verbose_name='Enabled'), + ), + ] diff --git a/mayan/apps/documents/models/document_models.py b/mayan/apps/documents/models/document_models.py index 0934a95f02..8b6fef2400 100644 --- a/mayan/apps/documents/models/document_models.py +++ b/mayan/apps/documents/models/document_models.py @@ -236,6 +236,18 @@ class Document(models.Model): def page_count(self): return self.latest_version.page_count + @property + def pages_all(self): + try: + return self.latest_version.pages_all + except AttributeError: + # Document has no version yet + DocumentPage = apps.get_model( + app_label='documents', model_name='DocumentPage' + ) + + return DocumentPage.objects.none() + @property def pages(self): try: diff --git a/mayan/apps/documents/models/document_page_models.py b/mayan/apps/documents/models/document_page_models.py index cfe3fb6be6..0cf3c26334 100644 --- a/mayan/apps/documents/models/document_page_models.py +++ b/mayan/apps/documents/models/document_page_models.py @@ -38,15 +38,17 @@ class DocumentPage(models.Model): Model that describes a document version page """ document_version = models.ForeignKey( - on_delete=models.CASCADE, related_name='pages', to=DocumentVersion, + on_delete=models.CASCADE, related_name='version_pages', to=DocumentVersion, verbose_name=_('Document version') ) + enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) page_number = models.PositiveIntegerField( db_index=True, default=1, editable=False, verbose_name=_('Page number') ) objects = DocumentPageManager() + passthrough = models.Manager() class Meta: ordering = ('page_number',) @@ -244,7 +246,7 @@ class DocumentPage(models.Model): ) % { 'document': force_text(self.document), 'page_num': self.page_number, - 'total_pages': self.document_version.pages.count() + 'total_pages': self.document_version.pages_all.count() } get_label.short_description = _('Label') diff --git a/mayan/apps/documents/models/document_version_models.py b/mayan/apps/documents/models/document_version_models.py index be47d6329b..e18e474fae 100644 --- a/mayan/apps/documents/models/document_version_models.py +++ b/mayan/apps/documents/models/document_version_models.py @@ -246,6 +246,17 @@ class DocumentVersion(models.Model): return result + @property + def pages_all(self): + DocumentPage = apps.get_model( + app_label='documents', model_name='DocumentPage' + ) + return DocumentPage.passthrough.filter(document_version=self) + + @property + def pages(self): + return self.version_pages.all() + @property def page_count(self): """ diff --git a/mayan/apps/documents/tasks.py b/mayan/apps/documents/tasks.py index 98e08d1e25..b77ca1e0b4 100644 --- a/mayan/apps/documents/tasks.py +++ b/mayan/apps/documents/tasks.py @@ -70,8 +70,7 @@ def task_generate_document_page_image(document_page_id, *args, **kwargs): app_label='documents', model_name='DocumentPage' ) - document_page = DocumentPage.objects.get(pk=document_page_id) - + document_page = DocumentPage.passthrough.get(pk=document_page_id) return document_page.generate_image(*args, **kwargs) diff --git a/mayan/apps/documents/tests/test_document_page_views.py b/mayan/apps/documents/tests/test_document_page_views.py index 57752396de..29ee28cec3 100644 --- a/mayan/apps/documents/tests/test_document_page_views.py +++ b/mayan/apps/documents/tests/test_document_page_views.py @@ -2,11 +2,152 @@ from __future__ import unicode_literals from django.utils.encoding import force_text -from ..permissions import permission_document_view +from ..permissions import ( + permission_document_edit, permission_document_view +) from .base import GenericDocumentViewTestCase +class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase): + def setUp(self): + super(DocumentPageDisableViewTestCase, self).setUp() + self.test_document_page = self.test_document.pages_all.first() + + def _request_test_document_page_disable_view(self): + return self.post( + viewname='documents:document_page_disable', kwargs={ + 'pk': self.test_document_page.pk + } + ) + + def test_document_page_disable_view_no_permission(self): + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_disable_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def test_document_page_disable_view_with_access(self): + self.grant_access( + obj=self.test_document, permission=permission_document_edit + ) + + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_disable_view() + self.assertEqual(response.status_code, 302) + + self.assertNotEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def _request_test_document_page_multiple_disable_view(self): + return self.post( + viewname='documents:document_page_multiple_disable', data={ + 'id_list': self.test_document_page.pk + } + ) + + def test_document_page_multiple_disable_view_no_permission(self): + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_multiple_disable_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def test_document_page_multiple_disable_view_with_access(self): + self.grant_access( + obj=self.test_document, permission=permission_document_edit + ) + + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_multiple_disable_view() + self.assertEqual(response.status_code, 302) + + self.assertNotEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def _disable_test_document_page(self): + self.test_document_page.enabled = False + self.test_document_page.save() + + def _request_test_document_page_enable_view(self): + return self.post( + viewname='documents:document_page_enable', kwargs={ + 'pk': self.test_document_page.pk + } + ) + + def test_document_page_enable_view_no_permission(self): + self._disable_test_document_page() + + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_enable_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def test_document_page_enable_view_with_access(self): + self._disable_test_document_page() + self.grant_access( + obj=self.test_document, permission=permission_document_edit + ) + + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_enable_view() + self.assertEqual(response.status_code, 302) + + self.assertNotEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def _request_test_document_page_multiple_enable_view(self): + return self.post( + viewname='documents:document_page_multiple_enable', data={ + 'id_list': self.test_document_page.pk + } + ) + + def test_document_page_multiple_enable_view_no_permission(self): + self._disable_test_document_page() + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_multiple_enable_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual( + test_document_page_count, self.test_document.pages.count() + ) + + def test_document_page_multiple_enable_view_with_access(self): + self._disable_test_document_page() + self.grant_access( + obj=self.test_document, permission=permission_document_edit + ) + + test_document_page_count = self.test_document.pages.count() + + response = self._request_test_document_page_multiple_enable_view() + self.assertEqual(response.status_code, 302) + + self.assertNotEqual( + test_document_page_count, self.test_document.pages.count() + ) + + class DocumentPageViewTestCase(GenericDocumentViewTestCase): def _request_test_document_page_list_view(self): return self.get( diff --git a/mayan/apps/documents/urls.py b/mayan/apps/documents/urls.py index e36181662e..bef5d0174a 100644 --- a/mayan/apps/documents/urls.py +++ b/mayan/apps/documents/urls.py @@ -32,6 +32,9 @@ from .views import ( RecentAccessDocumentListView, RecentAddedDocumentListView, ScanDuplicatedDocuments ) +from .views.document_page_views import ( + DocumentPageDisable, DocumentPageEnable +) from .views.document_type_views import DocumentTypeDeletionPoliciesEditView from .views.favorite_document_views import ( FavoriteAddView, FavoriteDocumentListView, FavoriteRemoveView @@ -266,7 +269,6 @@ urlpatterns = [ regex=r'^(?P\d+)/pages/all/$', view=DocumentPageListView.as_view(), name='document_pages' ), - url( regex=r'^multiple/clear_transformations/$', view=DocumentTransformationsClearView.as_view(), @@ -276,6 +278,22 @@ urlpatterns = [ regex=r'^page/(?P\d+)/$', view=DocumentPageView.as_view(), name='document_page_view' ), + url( + regex=r'^pages/(?P\d+)/disable/$', + name='document_page_disable', view=DocumentPageDisable.as_view() + ), + url( + regex=r'^pages/multiple/disable/$', name='document_page_multiple_disable', + view=DocumentPageDisable.as_view() + ), + url( + regex=r'^pages/(?P\d+)/enable/$', + name='document_page_enable', view=DocumentPageEnable.as_view() + ), + url( + regex=r'^pages/multiple/enable/$', name='document_page_multiple_enable', + view=DocumentPageEnable.as_view() + ), url( regex=r'^page/(?P\d+)/navigation/next/$', view=DocumentPageNavigationNext.as_view(), diff --git a/mayan/apps/documents/views/document_page_views.py b/mayan/apps/documents/views/document_page_views.py index e3dac8c3b8..81592f8fa8 100644 --- a/mayan/apps/documents/views/document_page_views.py +++ b/mayan/apps/documents/views/document_page_views.py @@ -7,10 +7,12 @@ from furl import furl from django.contrib import messages from django.urls import reverse from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ungettext from django.views.generic import RedirectView -from mayan.apps.common.generics import SimpleView, SingleObjectListView +from mayan.apps.common.generics import ( + MultipleObjectConfirmActionView, SimpleView, SingleObjectListView +) from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.common.settings import setting_home_view from mayan.apps.common.utils import resolve @@ -20,19 +22,20 @@ from ..forms import DocumentPageForm from ..icons import icon_document_pages from ..links import link_document_update_page_count from ..models import Document, DocumentPage -from ..permissions import permission_document_view +from ..permissions import permission_document_edit, permission_document_view from ..settings import ( setting_rotation_step, setting_zoom_percent_step, setting_zoom_max_level, setting_zoom_min_level ) __all__ = ( - 'DocumentPageListView', 'DocumentPageNavigationFirst', - 'DocumentPageNavigationLast', 'DocumentPageNavigationNext', - 'DocumentPageNavigationPrevious', 'DocumentPageView', - 'DocumentPageViewResetView', 'DocumentPageInteractiveTransformation', - 'DocumentPageZoomInView', 'DocumentPageZoomOutView', - 'DocumentPageRotateLeftView', 'DocumentPageRotateRightView' + 'DocumentPageDisable', 'DocumentPageEnable', 'DocumentPageListView', + 'DocumentPageNavigationFirst', 'DocumentPageNavigationLast', + 'DocumentPageNavigationNext', 'DocumentPageNavigationPrevious', + 'DocumentPageView', 'DocumentPageViewResetView', + 'DocumentPageInteractiveTransformation', 'DocumentPageZoomInView', + 'DocumentPageZoomOutView', 'DocumentPageRotateLeftView', + 'DocumentPageRotateRightView' ) logger = logging.getLogger(__name__) @@ -62,7 +65,7 @@ class DocumentPageListView(ExternalObjectMixin, SingleObjectListView): } def get_source_queryset(self): - return self.external_object.pages.all() + return self.external_object.pages_all class DocumentPageNavigationBase(ExternalObjectMixin, RedirectView): @@ -129,9 +132,9 @@ class DocumentPageNavigationNext(DocumentPageNavigationBase): document_page = self.get_object() try: - document_page = document_page.siblings.get( - page_number=document_page.page_number + 1 - ) + document_page = document_page.siblings.filter( + page_number__gt=document_page.page_number + ).first() except DocumentPage.DoesNotExist: messages.warning( message=_( @@ -147,9 +150,9 @@ class DocumentPageNavigationPrevious(DocumentPageNavigationBase): document_page = self.get_object() try: - document_page = document_page.siblings.get( - page_number=document_page.page_number - 1 - ) + document_page = document_page.siblings.filter( + page_number__lt=document_page.page_number + ).last() except DocumentPage.DoesNotExist: messages.warning( message=_( @@ -261,3 +264,63 @@ class DocumentPageRotateRightView(DocumentPageInteractiveTransformation): query_dict['rotation'] = ( int(query_dict['rotation']) + setting_rotation_step.value ) % 360 + + +class DocumentPageDisable(MultipleObjectConfirmActionView): + object_permission = permission_document_edit + pk_url_kwarg = 'pk' + success_message_singular = '%(count)d document page disabled.' + success_message_plural = '%(count)d document pages disabled.' + + def get_extra_context(self): + queryset = self.object_list + + result = { + 'title': ungettext( + singular='Disable the selected document page?', + plural='Disable the selected document pages?', + number=queryset.count() + ) + } + + if queryset.count() == 1: + result['object'] = queryset.first() + + return result + + def get_source_queryset(self): + return DocumentPage.passthrough.all() + + def object_action(self, form, instance): + instance.enabled = False + instance.save() + + +class DocumentPageEnable(MultipleObjectConfirmActionView): + object_permission = permission_document_edit + pk_url_kwarg = 'pk' + success_message_singular = '%(count)d document page enabled.' + success_message_plural = '%(count)d document pages enabled.' + + def get_extra_context(self): + queryset = self.object_list + + result = { + 'title': ungettext( + singular='Enable the selected document page?', + plural='Enable the selected document pages?', + number=queryset.count() + ) + } + + if queryset.count() == 1: + result['object'] = queryset.first() + + return result + + def get_source_queryset(self): + return DocumentPage.passthrough.all() + + def object_action(self, form, instance): + instance.enabled = True + instance.save() diff --git a/mayan/apps/ocr/apps.py b/mayan/apps/ocr/apps.py index 93a7e34d70..574a262507 100644 --- a/mayan/apps/ocr/apps.py +++ b/mayan/apps/ocr/apps.py @@ -83,7 +83,7 @@ class OCRApp(MayanAppConfig): ) ModelField( - model=Document, name='versions__pages__ocr_content__content' + model=Document, name='versions__version_pages__ocr_content__content' ) ModelPermission.register( @@ -114,7 +114,7 @@ class OCRApp(MayanAppConfig): ) document_search.add_model_field( - field='versions__pages__ocr_content__content', label=_('OCR') + field='versions__version_pages__ocr_content__content', label=_('OCR') ) document_page_search.add_model_field(