diff --git a/docs/releases/3.3.rst b/docs/releases/3.3.rst index 17ff89a513..47878877f9 100644 --- a/docs/releases/3.3.rst +++ b/docs/releases/3.3.rst @@ -93,7 +93,7 @@ Changes Removals -------- -- Database conversion. Reason for removal. The database conversions support +- Database conversion. Reason for removal: The database conversions support provided by this feature (SQLite to PostgreSQL) was being confused with database migrations and upgrades. @@ -108,7 +108,7 @@ Removals Continued confusion about the purpose of the feature and confusion about how errors with this feature were a reflexion of the code quality of - Mayannecessitated the removal of the database conversion feature. + Mayan necessitated the removal of the database conversion feature. - Django environ diff --git a/mayan/__init__.py b/mayan/__init__.py index 9bd0dcd8fd..9401b78d20 100644 --- a/mayan/__init__.py +++ b/mayan/__init__.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals __title__ = 'Mayan EDMS' __version__ = '3.3beta1' -__build__ = 0x030208 -__build_string__ = 'v3.2.8-255-g69086d87dd_Tue Oct 8 09:43:10 2019 -0400' +__build__ = 0x030300 +__build_string__ = 'v3.3beta1_Sat Oct 5 15:08:53 2019 -0400' __django_version__ = '1.11' __author__ = 'Roberto Rosario' __author_email__ = 'roberto.rosario@mayan-edms.com' diff --git a/mayan/apps/cabinets/apps.py b/mayan/apps/cabinets/apps.py index 6011bd4874..a2a0dc24bd 100644 --- a/mayan/apps/cabinets/apps.py +++ b/mayan/apps/cabinets/apps.py @@ -10,12 +10,14 @@ from mayan.apps.common.menus import ( menu_facet, menu_list_facet, menu_main, menu_multi_item, menu_object, menu_secondary ) +from mayan.apps.documents.search import ( + document_page_search, document_search, document_version_page_search +) from mayan.apps.events.classes import ModelEventType from mayan.apps.events.links import ( link_events_for_object, link_object_event_types_user_subcriptions_list, ) from mayan.apps.events.permissions import permission_events_view -from mayan.apps.documents.search import document_page_search, document_search from mayan.apps.navigation.classes import SourceColumn from .dependencies import * # NOQA @@ -115,12 +117,16 @@ class CabinetsApp(MayanAppConfig): ) document_page_search.add_model_field( - field='document_version__document__cabinets__label', + field='document__cabinets__label', label=_('Cabinets') ) document_search.add_model_field( field='cabinets__label', label=_('Cabinets') ) + document_version_page_search.add_model_field( + field='document_version__document__cabinets__label', + label=_('Cabinets') + ) menu_facet.bind_links( links=(link_document_cabinet_list,), sources=(Document,) diff --git a/mayan/apps/common/generics.py b/mayan/apps/common/generics.py index 37f5c268ea..310717743a 100644 --- a/mayan/apps/common/generics.py +++ b/mayan/apps/common/generics.py @@ -53,8 +53,8 @@ class MultiFormView(DjangoFormView): template_name = 'appearance/generic_form.html' def _create_form(self, form_name, klass): - form_kwargs = self.get_form_kwargs(form_name) - form_create_method = 'create_%s_form' % form_name + form_kwargs = self.get_form_kwargs(form_name=form_name) + form_create_method = 'create_{}_form'.format(form_name) if hasattr(self, form_create_method): form = getattr(self, form_create_method)(**form_kwargs) else: @@ -71,7 +71,7 @@ class MultiFormView(DjangoFormView): def forms_valid(self, forms): for form_name, form in forms.items(): - form_valid_method = '%s_form_valid' % form_name + form_valid_method = '{}_form_valid'.format(form_name) if hasattr(self, form_valid_method): return getattr(self, form_valid_method)(form) @@ -98,8 +98,8 @@ class MultiFormView(DjangoFormView): def get_form_kwargs(self, form_name): kwargs = {} - kwargs.update({'initial': self.get_initial(form_name)}) - kwargs.update({'prefix': self.get_prefix(form_name)}) + kwargs.update({'initial': self.get_initial(form_name=form_name)}) + kwargs.update({'prefix': self.get_prefix(form_name=form_name)}) if self.request.method in ('POST', 'PUT'): kwargs.update({ @@ -124,7 +124,7 @@ class MultiFormView(DjangoFormView): ) def get_initial(self, form_name): - initial_method = 'get_%s_initial' % form_name + initial_method = 'get_{}_initial'.format(form_name) if hasattr(self, initial_method): return getattr(self, initial_method)() else: diff --git a/mayan/apps/document_comments/apps.py b/mayan/apps/document_comments/apps.py index 371d7d9be4..ee34bb8038 100644 --- a/mayan/apps/document_comments/apps.py +++ b/mayan/apps/document_comments/apps.py @@ -8,7 +8,9 @@ from mayan.apps.common.apps import MayanAppConfig from mayan.apps.common.menus import ( menu_facet, menu_list_facet, menu_object, menu_secondary ) -from mayan.apps.documents.search import document_page_search, document_search +from mayan.apps.documents.search import ( + document_page_search, document_search, document_version_page_search +) from mayan.apps.events.classes import ModelEventType from mayan.apps.events.links import ( link_events_for_object, link_object_event_types_user_subcriptions_list @@ -80,13 +82,17 @@ class DocumentCommentsApp(MayanAppConfig): SourceColumn(attribute='comment', source=Comment) document_page_search.add_model_field( - field='document_version__document__comments__comment', + field='document__comments__comment', label=_('Comments') ) document_search.add_model_field( field='comments__comment', label=_('Comments') ) + document_version_page_search.add_model_field( + field='document_version__document__comments__comment', + label=_('Comments') + ) menu_facet.bind_links( links=(link_comments_for_document,), sources=(Document,) diff --git a/mayan/apps/document_parsing/admin.py b/mayan/apps/document_parsing/admin.py index 258da5ec3d..5fb8ca6a37 100644 --- a/mayan/apps/document_parsing/admin.py +++ b/mayan/apps/document_parsing/admin.py @@ -3,13 +3,13 @@ from __future__ import unicode_literals from django.contrib import admin from .models import ( - DocumentPageContent, DocumentVersionParseError + DocumentVersionPageContent, DocumentVersionParseError ) -@admin.register(DocumentPageContent) -class DocumentPageContentAdmin(admin.ModelAdmin): - list_display = ('document_page',) +@admin.register(DocumentVersionPageContent) +class DocumentVersionPageContentAdmin(admin.ModelAdmin): + list_display = ('document_version_page',) @admin.register(DocumentVersionParseError) diff --git a/mayan/apps/document_parsing/api_views.py b/mayan/apps/document_parsing/api_views.py index 7d29b61d94..ce024f34be 100644 --- a/mayan/apps/document_parsing/api_views.py +++ b/mayan/apps/document_parsing/api_views.py @@ -8,7 +8,7 @@ from rest_framework.response import Response from mayan.apps.documents.models import Document from mayan.apps.rest_api.permissions import MayanPermission -from .models import DocumentPageContent +from .models import DocumentVersionPageContent from .permissions import permission_content_view from .serializers import DocumentPageContentSerializer @@ -41,8 +41,8 @@ class APIDocumentPageContentView(generics.RetrieveAPIView): try: content = instance.content - except DocumentPageContent.DoesNotExist: - content = DocumentPageContent.objects.none() + except DocumentVersionPageContent.DoesNotExist: + content = DocumentVersionPageContent.objects.none() serializer = self.get_serializer(content) return Response(serializer.data) diff --git a/mayan/apps/document_parsing/apps.py b/mayan/apps/document_parsing/apps.py index ea4355c5a8..c7bd4e4773 100644 --- a/mayan/apps/document_parsing/apps.py +++ b/mayan/apps/document_parsing/apps.py @@ -12,7 +12,9 @@ from mayan.apps.common.classes import ModelField from mayan.apps.common.menus import ( menu_facet, menu_list_facet, menu_multi_item, menu_secondary, menu_tools ) -from mayan.apps.documents.search import document_search, document_page_search +from mayan.apps.documents.search import ( + document_search, document_page_search, document_version_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 @@ -43,7 +45,7 @@ from .permissions import ( permission_parse_document ) from .signals import post_document_version_parsing -from .utils import get_document_content +from .utils import get_document_content, get_document_version_content logger = logging.getLogger(__name__) @@ -74,6 +76,9 @@ class DocumentParsingApp(MayanAppConfig): DocumentVersion = apps.get_model( app_label='documents', model_name='DocumentVersion' ) + DocumentVersionPage = apps.get_model( + app_label='documents', model_name='DocumentVersionPage' + ) DocumentVersionParseError = self.get_model( model_name='DocumentVersionParseError' ) @@ -85,7 +90,7 @@ class DocumentParsingApp(MayanAppConfig): name='content', value=get_document_content ) DocumentVersion.add_to_class( - name='content', value=get_document_content + name='content', value=get_document_version_content ) DocumentVersion.add_to_class( name='submit_for_parsing', @@ -100,9 +105,9 @@ class DocumentParsingApp(MayanAppConfig): ) ) - ModelField( - model=Document, name='versions__version_pages__content__content' - ) + #ModelField( + # model=Document, name='versions__pages__content__content' + #) ModelPermission.register( model=Document, permissions=( @@ -133,17 +138,17 @@ class DocumentParsingApp(MayanAppConfig): ) document_search.add_model_field( - field='versions__version_pages__content__content', label=_('Content') + field='versions__pages__content__content', label=_('Content') ) - document_page_search.add_model_field( + document_version_page_search.add_model_field( field='content__content', label=_('Content') ) menu_facet.bind_links( links=(link_document_content,), sources=(Document,) ) - menu_facet.bind_links( + menu_list_facet.bind_links( links=(link_document_page_content,), sources=(DocumentPage,) ) menu_list_facet.bind_links( diff --git a/mayan/apps/document_parsing/forms.py b/mayan/apps/document_parsing/forms.py index 5c4d6af26d..2803c9cd59 100644 --- a/mayan/apps/document_parsing/forms.py +++ b/mayan/apps/document_parsing/forms.py @@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext from mayan.apps.common.widgets import TextAreaDiv -from .models import DocumentPageContent +from .models import DocumentVersionPageContent class DocumentContentForm(forms.Form): @@ -26,10 +26,10 @@ class DocumentContentForm(forms.Form): except AttributeError: document_pages = [] - for page in document_pages: + for document_page in document_pages: try: - page_content = page.content.content - except DocumentPageContent.DoesNotExist: + page_content = document_page.content_object.content.content + except DocumentVersionPageContent.DoesNotExist: pass else: content.append(conditional_escape(force_text(page_content))) @@ -37,7 +37,7 @@ class DocumentContentForm(forms.Form): '\n\n\n
- %s -

\n\n\n' % ( ugettext( 'Page %(page_number)d' - ) % {'page_number': page.page_number} + ) % {'page_number': document_page.page_number} ) ) @@ -72,8 +72,8 @@ class DocumentPageContentForm(forms.Form): self.fields['contents'].initial = '' try: - page_content = document_page.content.content - except DocumentPageContent.DoesNotExist: + page_content = document_page.content_object.content.content + except DocumentVersionPageContent.DoesNotExist: pass else: content = conditional_escape(force_text(page_content)) diff --git a/mayan/apps/document_parsing/icons.py b/mayan/apps/document_parsing/icons.py index 6aad4a8ea2..9fb872dc1f 100644 --- a/mayan/apps/document_parsing/icons.py +++ b/mayan/apps/document_parsing/icons.py @@ -17,6 +17,9 @@ icon_document_content_download = Icon( icon_document_multiple_submit = Icon( driver_name='fontawesome', symbol='font' ) +icon_document_page_content = Icon( + driver_name='fontawesome', symbol='font' +) icon_document_submit = Icon( driver_name='fontawesome', symbol='font' ) diff --git a/mayan/apps/document_parsing/links.py b/mayan/apps/document_parsing/links.py index 2cfe95fab9..a7de474581 100644 --- a/mayan/apps/document_parsing/links.py +++ b/mayan/apps/document_parsing/links.py @@ -32,9 +32,15 @@ link_document_content_delete_multiple = Link( text=_('Delete parsed content'), view='document_parsing:document_content_delete_multiple', ) +link_document_content_download = Link( + args='resolved_object.id', + icon_class_path='mayan.apps.document_parsing.icons.icon_document_content_download', + permissions=(permission_content_view,), text=_('Download content'), + view='document_parsing:document_content_download' +) link_document_page_content = Link( args='resolved_object.id', conditional_disable=is_document_page_disabled, - icon_class_path='mayan.apps.document_parsing.icons.icon_document_content', + icon_class_path='mayan.apps.document_parsing.icons.icon_document_page_content', permissions=(permission_content_view,), text=_('Content'), view='document_parsing:document_page_content' ) @@ -44,12 +50,6 @@ link_document_parsing_errors_list = Link( permissions=(permission_content_view,), text=_('Parsing errors'), view='document_parsing:document_parsing_error_list' ) -link_document_content_download = Link( - args='resolved_object.id', - icon_class_path='mayan.apps.document_parsing.icons.icon_document_content_download', - permissions=(permission_content_view,), text=_('Download content'), - view='document_parsing:document_content_download' -) link_document_submit_multiple = Link( icon_class_path='mayan.apps.document_parsing.icons.icon_document_submit', text=_('Submit for parsing'), diff --git a/mayan/apps/document_parsing/managers.py b/mayan/apps/document_parsing/managers.py index 1669f89e70..8ff1d5d231 100644 --- a/mayan/apps/document_parsing/managers.py +++ b/mayan/apps/document_parsing/managers.py @@ -18,11 +18,13 @@ from .signals import post_document_version_parsing logger = logging.getLogger(__name__) -class DocumentPageContentManager(models.Manager): +class DocumentVersionPageContentManager(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() + self.filter( + document_version_page=document_page.content_object + ).delete() event_parsing_document_content_deleted.commit( actor=user, target=document diff --git a/mayan/apps/document_parsing/migrations/0005_rename_page_content.py b/mayan/apps/document_parsing/migrations/0005_rename_page_content.py new file mode 100644 index 0000000000..11ad6cdc8b --- /dev/null +++ b/mayan/apps/document_parsing/migrations/0005_rename_page_content.py @@ -0,0 +1,42 @@ +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ('document_parsing', '0004_auto_20180917_0645'), + ('documents', '0052_rename_document_page'), + ] + + operations = [ + migrations.RenameModel( + 'DocumentPageContent', 'DocumentVersionPageContent' + ), + migrations.AlterField( + model_name='documentversionpagecontent', + name='document_page', + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name='content', + to='documents.DocumentVersionPage', + verbose_name='Document version page' + ), + ), + migrations.RenameField( + model_name='documentversionpagecontent', + old_name='document_page', + new_name='document_version_page', + ), + migrations.AlterModelOptions( + name='documentversionpagecontent', + options={ + 'verbose_name': 'Document version page content', + 'verbose_name_plural': 'Document version pages contents' + }, + ), + ] + + + diff --git a/mayan/apps/document_parsing/models.py b/mayan/apps/document_parsing/models.py index 5e9b52320e..48c2dc79af 100644 --- a/mayan/apps/document_parsing/models.py +++ b/mayan/apps/document_parsing/models.py @@ -5,36 +5,12 @@ from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from mayan.apps.documents.models import ( - DocumentPage, DocumentType, DocumentVersion + DocumentPage, DocumentType, DocumentVersion, DocumentVersionPage ) -from .managers import DocumentPageContentManager, DocumentTypeSettingsManager - - -@python_2_unicode_compatible -class DocumentPageContent(models.Model): - """ - This model store's the parsed content of a document page. - """ - document_page = models.OneToOneField( - on_delete=models.CASCADE, related_name='content', to=DocumentPage, - verbose_name=_('Document page') - ) - content = models.TextField( - blank=True, help_text=_( - 'The actual text content as extracted by the document ' - 'parsing backend.' - ), verbose_name=_('Content') - ) - - objects = DocumentPageContentManager() - - class Meta: - verbose_name = _('Document page content') - verbose_name_plural = _('Document pages contents') - - def __str__(self): - return force_text(self.document_page) +from .managers import ( + DocumentVersionPageContentManager, DocumentTypeSettingsManager +) class DocumentTypeSettings(models.Model): @@ -62,6 +38,32 @@ class DocumentTypeSettings(models.Model): verbose_name_plural = _('Document types settings') +@python_2_unicode_compatible +class DocumentVersionPageContent(models.Model): + """ + This model store's the parsed content of a document page. + """ + document_version_page = models.OneToOneField( + on_delete=models.CASCADE, related_name='content', + to=DocumentVersionPage, verbose_name=_('Document version page') + ) + content = models.TextField( + blank=True, help_text=_( + 'The actual text content as extracted by the document ' + 'parsing backend.' + ), verbose_name=_('Content') + ) + + objects = DocumentVersionPageContentManager() + + class Meta: + verbose_name = _('Document version page content') + verbose_name_plural = _('Document version pages contents') + + def __str__(self): + return force_text(self.document_page) + + @python_2_unicode_compatible class DocumentVersionParseError(models.Model): """ diff --git a/mayan/apps/document_parsing/parsers.py b/mayan/apps/document_parsing/parsers.py index 0e4d0cff70..469b974377 100644 --- a/mayan/apps/document_parsing/parsers.py +++ b/mayan/apps/document_parsing/parsers.py @@ -23,11 +23,13 @@ class Parser(object): _registry = {} @classmethod - def parse_document_page(cls, document_page): - for parser_class in cls._registry.get(document_page.document_version.mimetype, ()): + def parse_document_version_page(cls, document_version_page): + for parser_class in cls._registry.get(document_version_page.document_version.mimetype, ()): try: parser = parser_class() - parser.process_document_page(document_page) + parser.process_document_page( + document_version_page=document_version_page + ) except ParserError: # If parser raises error, try next parser in the list pass @@ -41,7 +43,9 @@ class Parser(object): for parser_class in cls._registry.get(document_version.mimetype, ()): try: parser = parser_class() - parser.process_document_version(document_version) + parser.process_document_version( + document_version=document_version + ) except ParserError: # If parser raises error, try next parser in the list pass @@ -64,29 +68,33 @@ class Parser(object): ) logger.debug('document version: %d', document_version.pk) - for document_page in document_version.pages.all(): - self.process_document_page(document_page=document_page) + for document_version_page in document_version.pages.all(): + self.process_document_version_page( + document_version_page=document_version_page + ) - def process_document_page(self, document_page): - DocumentPageContent = apps.get_model( - app_label='document_parsing', model_name='DocumentPageContent' + def process_document_version_page(self, document_version_page): + DocumentVersionPageContent = apps.get_model( + app_label='document_parsing', + model_name='DocumentVersionPageContent' ) logger.info( 'Processing page: %d of document version: %s', - document_page.page_number, document_page.document_version + document_version_page.page_number, + document_version_page.document_version ) - file_object = document_page.document_version.get_intermediate_file() + file_object = document_version_page.document_version.get_intermediate_file() try: - document_page_content, created = DocumentPageContent.objects.get_or_create( - document_page=document_page + document_version_page_content, created = DocumentVersionPageContent.objects.get_or_create( + document_version_page=document_version_page ) - document_page_content.content = self.execute( - file_object=file_object, page_number=document_page.page_number + document_version_page_content.content = self.execute( + file_object=file_object, page_number=document_version_page.page_number ) - document_page_content.save() + document_version_page_content.save() except Exception as exception: error_message = _('Exception parsing page; %s') % exception logger.error(error_message) @@ -96,7 +104,8 @@ class Parser(object): logger.info( 'Finished processing page: %d of document version: %s', - document_page.page_number, document_page.document_version + document_version_page.page_number, + document_version_page.document_version ) def execute(self, file_object, page_number): diff --git a/mayan/apps/document_parsing/serializers.py b/mayan/apps/document_parsing/serializers.py index 7161d2fc40..53d90d354e 100644 --- a/mayan/apps/document_parsing/serializers.py +++ b/mayan/apps/document_parsing/serializers.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals from rest_framework import serializers -from .models import DocumentPageContent +from .models import DocumentVersionPageContent class DocumentPageContentSerializer(serializers.ModelSerializer): class Meta: fields = ('content',) - model = DocumentPageContent + model = DocumentVersionPageContent diff --git a/mayan/apps/document_parsing/tasks.py b/mayan/apps/document_parsing/tasks.py index 4debffbc60..653552f741 100644 --- a/mayan/apps/document_parsing/tasks.py +++ b/mayan/apps/document_parsing/tasks.py @@ -14,8 +14,8 @@ def task_parse_document_version(document_version_pk): DocumentVersion = apps.get_model( app_label='documents', model_name='DocumentVersion' ) - DocumentPageContent = apps.get_model( - app_label='document_parsing', model_name='DocumentPageContent' + DocumentVersionPageContent = apps.get_model( + app_label='document_parsing', model_name='DocumentVersionPageContent' ) document_version = DocumentVersion.objects.get( @@ -24,6 +24,6 @@ def task_parse_document_version(document_version_pk): logger.info( 'Starting parsing for document version: %s', document_version ) - DocumentPageContent.objects.process_document_version( + DocumentVersionPageContent.objects.process_document_version( document_version=document_version ) diff --git a/mayan/apps/document_parsing/tests/test_events.py b/mayan/apps/document_parsing/tests/test_events.py index 1a2860305d..17bbe198fb 100644 --- a/mayan/apps/document_parsing/tests/test_events.py +++ b/mayan/apps/document_parsing/tests/test_events.py @@ -10,7 +10,7 @@ from ..events import ( event_parsing_document_version_submit, event_parsing_document_version_finish ) -from ..models import DocumentPageContent +from ..models import DocumentVersionPageContent class DocumentParsingEventsTestCase(GenericDocumentTestCase): @@ -19,7 +19,7 @@ class DocumentParsingEventsTestCase(GenericDocumentTestCase): def test_document_content_deleted_event(self): Action.objects.all().delete() - DocumentPageContent.objects.delete_content_for( + DocumentVersionPageContent.objects.delete_content_for( document=self.test_document ) diff --git a/mayan/apps/document_parsing/tests/test_parsers.py b/mayan/apps/document_parsing/tests/test_parsers.py index 237bccc567..2eeb8703a6 100644 --- a/mayan/apps/document_parsing/tests/test_parsers.py +++ b/mayan/apps/document_parsing/tests/test_parsers.py @@ -18,5 +18,5 @@ class ParserTestCase(DocumentTestMixin, BaseTestCase): parser.process_document_version(self.test_document.latest_version) self.assertTrue( - TEST_DOCUMENT_CONTENT in self.test_document.pages.first().content.content + TEST_DOCUMENT_CONTENT in self.test_document.pages.first().content_object.content.content ) diff --git a/mayan/apps/document_parsing/tests/test_views.py b/mayan/apps/document_parsing/tests/test_views.py index bb88c1817b..21ca0ef808 100644 --- a/mayan/apps/document_parsing/tests/test_views.py +++ b/mayan/apps/document_parsing/tests/test_views.py @@ -5,7 +5,7 @@ from django.test import override_settings from mayan.apps.documents.tests.base import GenericDocumentViewTestCase from mayan.apps.documents.tests.literals import TEST_HYBRID_DOCUMENT -from ..models import DocumentPageContent +from ..models import DocumentVersionPageContent from ..permissions import ( permission_content_view, permission_document_type_parsing_setup, permission_parse_document @@ -72,8 +72,8 @@ class DocumentContentViewsTestCase( self.assertEqual(response.status_code, 404) self.assertTrue( - DocumentPageContent.objects.filter( - document_page=self.test_document.pages.first() + DocumentVersionPageContent.objects.filter( + document_version_page=self.test_document.pages.first().content_object ).exists() ) @@ -86,8 +86,8 @@ class DocumentContentViewsTestCase( self.assertEqual(response.status_code, 302) self.assertFalse( - DocumentPageContent.objects.filter( - document_page=self.test_document.pages.first() + DocumentVersionPageContent.objects.filter( + document_version_page=self.test_document.pages.first().content_object ).exists() ) diff --git a/mayan/apps/document_parsing/urls.py b/mayan/apps/document_parsing/urls.py index f590236176..877ae67a4e 100644 --- a/mayan/apps/document_parsing/urls.py +++ b/mayan/apps/document_parsing/urls.py @@ -7,7 +7,9 @@ from .views import ( DocumentContentView, DocumentContentDeleteView, DocumentContentDownloadView, DocumentPageContentView, DocumentParsingErrorsListView, DocumentSubmitView, - DocumentTypeSettingsEditView, DocumentTypeSubmitView, ParseErrorListView + DocumentTypeSettingsEditView, DocumentTypeSubmitView, + DocumentVersionPageContentView, + ParseErrorListView ) urlpatterns = [ @@ -34,6 +36,11 @@ urlpatterns = [ regex=r'^documents/pages/(?P\d+)/content/$', view=DocumentPageContentView.as_view(), name='document_page_content' ), + url( + regex=r'^documents/versions/pages/(?P\d+)/content/$', + view=DocumentVersionPageContentView.as_view(), + name='document_version_page_content' + ), url( regex=r'^documents/(?P\d+)/submit/$', view=DocumentSubmitView.as_view(), name='document_submit' diff --git a/mayan/apps/document_parsing/utils.py b/mayan/apps/document_parsing/utils.py index ab8e049450..5086d5cf02 100644 --- a/mayan/apps/document_parsing/utils.py +++ b/mayan/apps/document_parsing/utils.py @@ -6,14 +6,28 @@ from django.utils.html import conditional_escape def get_document_content(document): - DocumentPageContent = apps.get_model( - app_label='document_parsing', model_name='DocumentPageContent' + DocumentVersionPageContent = apps.get_model( + app_label='document_parsing', model_name='DocumentVersionPageContent' ) - for page in document.pages.all(): + for document_page in document.pages.all(): try: - page_content = page.content.content - except DocumentPageContent.DoesNotExist: + page_content = document_page.content_object.content.content + except DocumentVersionPageContent.DoesNotExist: + pass + else: + yield conditional_escape(force_text(page_content)) + + +def get_document_version_content(document_version): + DocumentVersionPageContent = apps.get_model( + app_label='document_parsing', model_name='DocumentVersionPageContent' + ) + + for document_version_page in document_version.pages.all(): + try: + page_content = document_version_page.content.content + except DocumentVersionPageContent.DoesNotExist: pass else: yield conditional_escape(force_text(page_content)) diff --git a/mayan/apps/document_parsing/views.py b/mayan/apps/document_parsing/views.py index de9cb3ef0b..7201253c18 100644 --- a/mayan/apps/document_parsing/views.py +++ b/mayan/apps/document_parsing/views.py @@ -12,10 +12,12 @@ from mayan.apps.common.generics import ( ) from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.documents.forms import DocumentTypeFilteredSelectForm -from mayan.apps.documents.models import Document, DocumentPage, DocumentType +from mayan.apps.documents.models import ( + Document, DocumentPage, DocumentType, DocumentVersionPage +) from .forms import DocumentContentForm, DocumentPageContentForm -from .models import DocumentPageContent, DocumentVersionParseError +from .models import DocumentVersionPageContent, DocumentVersionParseError from .permissions import ( permission_content_view, permission_document_type_parsing_setup, permission_parse_document @@ -46,7 +48,7 @@ class DocumentContentDeleteView(MultipleObjectConfirmActionView): return result def object_action(self, form, instance): - DocumentPageContent.objects.delete_content_for( + DocumentVersionPageContent.objects.delete_content_for( document=instance, user=self.request.user ) @@ -107,6 +109,30 @@ class DocumentPageContentView(SingleObjectDetailView): } +class DocumentVersionPageContentView(SingleObjectDetailView): + form_class = DocumentPageContentForm + model = DocumentVersionPage + object_permission = permission_content_view + + def dispatch(self, request, *args, **kwargs): + result = super(DocumentPageContentView, self).dispatch( + request, *args, **kwargs + ) + self.get_object().document.add_as_recent_document_for_user( + request.user + ) + return result + + def get_extra_context(self): + return { + 'hide_labels': True, + 'object': self.get_object(), + 'title': _( + 'Content for document version page: %s' + ) % self.get_object(), + } + + class DocumentParsingErrorsListView(SingleObjectListView): view_permission = permission_content_view diff --git a/mayan/apps/documents/admin.py b/mayan/apps/documents/admin.py index 62d30c0e1f..59b6004f31 100644 --- a/mayan/apps/documents/admin.py +++ b/mayan/apps/documents/admin.py @@ -3,18 +3,12 @@ from __future__ import unicode_literals from django.contrib import admin from .models import ( - DeletedDocument, Document, DocumentPage, DocumentType, - DocumentTypeFilename, DocumentVersion, DuplicatedDocument, RecentDocument + DeletedDocument, Document, DocumentType, DocumentTypeFilename, + DocumentVersion, DocumentVersionPage, DuplicatedDocument, + RecentDocument ) -class DocumentPageInline(admin.StackedInline): - model = DocumentPage - extra = 1 - classes = ('collapse-open',) - allow_add = True - - class DocumentTypeFilenameInline(admin.StackedInline): model = DocumentTypeFilename extra = 1 @@ -29,6 +23,13 @@ class DocumentVersionInline(admin.StackedInline): allow_add = True +class DocumentVersionPageInline(admin.StackedInline): + model = DocumentVersionPage + extra = 1 + classes = ('collapse-open',) + allow_add = True + + @admin.register(DeletedDocument) class DeletedDocumentAdmin(admin.ModelAdmin): date_hierarchy = 'deleted_date_time' diff --git a/mayan/apps/documents/api_views.py b/mayan/apps/documents/api_views.py index 4d17038722..87c4c9bd3d 100644 --- a/mayan/apps/documents/api_views.py +++ b/mayan/apps/documents/api_views.py @@ -33,10 +33,14 @@ from .serializers import ( DocumentTypeSerializer, DocumentVersionSerializer, NewDocumentSerializer, NewDocumentVersionSerializer, RecentDocumentSerializer, WritableDocumentSerializer, - WritableDocumentTypeSerializer, WritableDocumentVersionSerializer + WritableDocumentTypeSerializer, WritableDocumentVersionSerializer, + DocumentVersionPageSerializer ) from .settings import settings_document_page_image_cache_time -from .tasks import task_generate_document_page_image +from .tasks import ( + task_generate_document_page_image, + task_generate_document_version_page_image +) logger = logging.getLogger(__name__) @@ -168,13 +172,8 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): ) return document - def get_document_version(self): - return get_object_or_404( - self.get_document().versions.all(), pk=self.kwargs['version_pk'] - ) - def get_queryset(self): - return self.get_document_version().pages_all.all() + return self.get_document().pages_all.all() def get_serializer(self, *args, **kwargs): return None @@ -221,6 +220,95 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): return response +class APIDocumentVersionPageImageView(generics.RetrieveAPIView): + """ + get: Returns an image representation of the selected document version page. + """ + lookup_url_kwarg = 'page_pk' + + def get_document(self): + if self.request.method == 'GET': + permission_required = permission_document_view + else: + permission_required = permission_document_edit + + document = get_object_or_404(Document.passthrough, pk=self.kwargs['pk']) + + AccessControlList.objects.check_access( + obj=document, permissions=(permission_required,), + user=self.request.user + ) + return document + + def get_document_version(self): + return get_object_or_404( + self.get_document().versions.all(), pk=self.kwargs['version_pk'] + ) + + def get_queryset(self): + return self.get_document_version().pages.all() + + def get_serializer(self, *args, **kwargs): + return None + + def get_serializer_class(self): + return None + + @cache_control(private=True) + def retrieve(self, request, *args, **kwargs): + width = request.GET.get('width') + height = request.GET.get('height') + zoom = request.GET.get('zoom') + + if zoom: + zoom = int(zoom) + + rotation = request.GET.get('rotation') + + if rotation: + rotation = int(rotation) + + maximum_layer_order = request.GET.get('maximum_layer_order') + if maximum_layer_order: + maximum_layer_order = int(maximum_layer_order) + + task = task_generate_document_version_page_image.apply_async( + kwargs=dict( + document_version_page_id=self.get_object().pk, width=width, + height=height, zoom=zoom, rotation=rotation, + maximum_layer_order=maximum_layer_order, + user_id=request.user.pk + ) + ) + + cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT) + cache_file = self.get_object().cache_partition.get_file(filename=cache_filename) + with cache_file.open() as file_object: + response = HttpResponse(file_object.read(), content_type='image') + if '_hash' in request.GET: + patch_cache_control( + response=response, + max_age=settings_document_page_image_cache_time.value + ) + return response + + +class APIDocumentPageListView(generics.ListAPIView): + serializer_class = DocumentPageSerializer + + def get_document(self): + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + AccessControlList.objects.check_access( + obj=document, permissions=(permission_document_view,), + user=self.request.user + ) + return document + + def get_queryset(self): + return self.get_document().pages.all() + + class APIDocumentPageView(generics.RetrieveUpdateAPIView): """ get: Returns the selected document page details. @@ -230,6 +318,33 @@ class APIDocumentPageView(generics.RetrieveUpdateAPIView): lookup_url_kwarg = 'page_pk' serializer_class = DocumentPageSerializer + def get_document(self): + if self.request.method == 'GET': + permission_required = permission_document_view + else: + permission_required = permission_document_edit + + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + AccessControlList.objects.check_access( + obj=document, permissions=(permission_required,), + user=self.request.user + ) + return document + + def get_queryset(self): + return self.get_document().pages.all() + + +class APIDocumentVersionPageView(generics.RetrieveUpdateAPIView): + """ + get: Returns the selected document verion page details. + patch: Edit the selected document version page. + put: Edit the selected document version page. + """ + lookup_url_kwarg = 'page_pk' + serializer_class = DocumentVersionPageSerializer + def get_document(self): if self.request.method == 'GET': permission_required = permission_document_view @@ -289,8 +404,7 @@ class APIDocumentTypeView(generics.RetrieveUpdateDestroyAPIView): 'GET': (permission_document_type_view,), 'PUT': (permission_document_type_edit,), 'PATCH': (permission_document_type_edit,), - 'DELETE': (permission_document_type_delete,) - } + 'DELETE': (permission_document_type_delete,) } permission_classes = (MayanPermission,) queryset = DocumentType.objects.all() @@ -423,7 +537,7 @@ class APIRecentDocumentListView(generics.ListAPIView): class APIDocumentVersionPageListView(generics.ListAPIView): - serializer_class = DocumentPageSerializer + serializer_class = DocumentVersionPageSerializer def get_document(self): document = get_object_or_404(Document, pk=self.kwargs['pk']) diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 09e85928c7..541134fb4b 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -59,7 +59,7 @@ from .links import ( link_document_multiple_delete, link_document_multiple_document_type_edit, 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_multiple_trash, link_document_multiple_pages_reset, 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, @@ -74,8 +74,10 @@ from .links import ( link_document_type_filename_create, link_document_type_filename_delete, link_document_type_filename_edit, link_document_type_filename_list, link_document_type_list, link_document_type_policies, - link_document_type_setup, link_document_update_page_count, + link_document_type_setup, link_document_pages_reset, link_document_version_download, link_document_version_list, + link_document_version_multiple_page_count_update, + link_document_version_page_count_update, link_document_version_return_document, link_document_version_return_list, link_document_version_revert, link_document_version_view, link_duplicated_document_list, link_duplicated_document_scan, @@ -87,10 +89,10 @@ from .permissions import ( permission_document_download, permission_document_edit, permission_document_new_version, permission_document_print, permission_document_properties_edit, permission_document_restore, - permission_document_trash, permission_document_type_delete, - permission_document_type_edit, permission_document_type_view, - permission_document_version_revert, permission_document_version_view, - permission_document_view + permission_document_tools, permission_document_trash, + permission_document_type_delete, permission_document_type_edit, + permission_document_type_view, permission_document_version_revert, + permission_document_version_view, permission_document_view, ) # Just import to initialize the search models from .search import document_search, document_page_search # NOQA @@ -121,10 +123,11 @@ class DocumentsApp(MayanAppConfig): DeletedDocument = self.get_model(model_name='DeletedDocument') Document = self.get_model(model_name='Document') DocumentPage = self.get_model(model_name='DocumentPage') - DocumentPageResult = self.get_model(model_name='DocumentPageResult') + DocumentPageResult = self.get_model(model_name='DocumentVersionPageResult') DocumentType = self.get_model(model_name='DocumentType') DocumentTypeFilename = self.get_model(model_name='DocumentTypeFilename') DocumentVersion = self.get_model(model_name='DocumentVersion') + DocumentVersionPage = self.get_model(model_name='DocumentVersionPage') DuplicatedDocument = self.get_model(model_name='DuplicatedDocument') DynamicSerializerField.add_serializer( @@ -190,13 +193,15 @@ class DocumentsApp(MayanAppConfig): permission_acl_edit, permission_acl_view, permission_document_delete, permission_document_download, permission_document_edit, permission_document_new_version, - permission_document_print, permission_document_properties_edit, - permission_document_restore, permission_document_trash, - permission_document_version_revert, + permission_document_print, + permission_document_properties_edit, + permission_document_restore, permission_document_tools, + permission_document_trash, permission_document_version_revert, permission_document_version_view, permission_document_view, permission_events_view, permission_transformation_create, permission_transformation_delete, - permission_transformation_edit, permission_transformation_view, + permission_transformation_edit, + permission_transformation_view, ) ) @@ -224,13 +229,13 @@ class DocumentsApp(MayanAppConfig): model=Document, manager_name='passthrough' ) ModelPermission.register_inheritance( - model=DocumentPage, related='document_version__document', + model=DocumentPage, related='document', ) ModelPermission.register_manager( model=DocumentPage, manager_name='passthrough' ) ModelPermission.register_inheritance( - model=DocumentPageResult, related='document_version__document', + model=DocumentPageResult, related='document', ) ModelPermission.register_manager( model=DocumentPageResult, manager_name='passthrough' @@ -241,6 +246,9 @@ class DocumentsApp(MayanAppConfig): ModelPermission.register_inheritance( model=DocumentVersion, related='document', ) + ModelPermission.register_inheritance( + model=DocumentVersionPage, related='document_version', + ) # Document and document page thumbnail widget document_page_thumbnail_widget = DocumentPageThumbnailWidget() @@ -454,7 +462,7 @@ class DocumentsApp(MayanAppConfig): link_document_quick_download, link_document_download, link_document_clear_transformations, link_document_clone_transformations, - link_document_update_page_count, + link_document_pages_reset, ), sources=(Document,) ) menu_object.bind_links( @@ -495,7 +503,7 @@ class DocumentsApp(MayanAppConfig): link_document_multiple_favorites_remove, link_document_multiple_clear_transformations, link_document_multiple_trash, link_document_multiple_download, - link_document_multiple_update_page_count, + link_document_multiple_pages_reset, link_document_multiple_document_type_edit, ), sources=(Document,) ) @@ -547,6 +555,17 @@ class DocumentsApp(MayanAppConfig): link_document_version_return_list ), sources=(DocumentVersion,) ) + menu_multi_item.bind_links( + links=( + link_document_version_multiple_page_count_update, + ), sources=(DocumentVersion,) + ) + menu_object.bind_links( + links=( + link_document_version_page_count_update, + ), sources=(DocumentVersion,) + ) + menu_list_facet.bind_links( links=(link_document_version_view,), sources=(DocumentVersion,) ) diff --git a/mayan/apps/documents/dashboard_widgets.py b/mayan/apps/documents/dashboard_widgets.py index 2ca9dd1b89..221e2cfbbd 100644 --- a/mayan/apps/documents/dashboard_widgets.py +++ b/mayan/apps/documents/dashboard_widgets.py @@ -32,12 +32,12 @@ class DashboardWidgetDocumentPagesTotal(DashboardWidgetNumeric): AccessControlList = apps.get_model( app_label='acls', model_name='AccessControlList' ) - DocumentPage = apps.get_model( - app_label='documents', model_name='DocumentPage' + DocumentVersionPage = apps.get_model( + app_label='documents', model_name='DocumentVersionPage' ) self.count = AccessControlList.objects.restrict_queryset( permission=permission_document_view, user=request.user, - queryset=DocumentPage.objects.all() + queryset=DocumentVersionPage.objects.all() ).count() return super(DashboardWidgetDocumentPagesTotal, self).render(request) diff --git a/mayan/apps/documents/icons.py b/mayan/apps/documents/icons.py index fb891fd585..ce365c8678 100644 --- a/mayan/apps/documents/icons.py +++ b/mayan/apps/documents/icons.py @@ -36,7 +36,10 @@ icon_document_edit = Icon( ) icon_document = Icon(driver_name='fontawesome', symbol='book') icon_document_list = icon_document -icon_document_page_count_update = Icon( +icon_document_pages_reset = Icon( + driver_name='fontawesome', symbol='copy' +) +icon_document_version_page_count_update = Icon( driver_name='fontawesome', symbol='copy' ) icon_document_preview = Icon(driver_name='fontawesome', symbol='eye') diff --git a/mayan/apps/documents/links.py b/mayan/apps/documents/links.py index 271aa16579..89cbd31782 100644 --- a/mayan/apps/documents/links.py +++ b/mayan/apps/documents/links.py @@ -168,12 +168,12 @@ link_document_quick_download = Link( permissions=(permission_document_download,), text=_('Quick download'), view='documents:document_download', ) -link_document_update_page_count = Link( +link_document_pages_reset = Link( args='resolved_object.pk', - icon_class_path='mayan.apps.documents.icons.icon_document_page_count_update', + icon_class_path='mayan.apps.documents.icons.icon_document_pages_reset', permissions=(permission_document_tools,), - text=_('Recalculate page count'), - view='documents:document_update_page_count' + text=_('Reset pages'), + view='documents:document_pages_reset' ) link_document_restore = Link( permissions=(permission_document_restore,), @@ -217,10 +217,10 @@ link_document_multiple_download = Link( text=_('Advanced download'), view='documents:document_multiple_download_form' ) -link_document_multiple_update_page_count = Link( - icon_class_path='mayan.apps.documents.icons.icon_document_page_count_update', - text=_('Recalculate page count'), - view='documents:document_multiple_update_page_count' +link_document_multiple_pages_reset = Link( + icon_class_path='mayan.apps.documents.icons.icon_document_pages_reset', + text=_('Reset pages'), + view='documents:document_multiple_pages_reset' ) link_document_multiple_restore = Link( icon_class_path='mayan.apps.documents.icons.icon_trashed_document_restore', @@ -246,6 +246,18 @@ link_document_version_return_list = Link( permissions=(permission_document_version_view,), text=_('Versions'), view='documents:document_version_list', ) +link_document_version_page_count_update = Link( + args='resolved_object.pk', + icon_class_path='mayan.apps.documents.icons.icon_document_version_page_count_update', + permissions=(permission_document_tools,), + text=_('Update page count'), + view='documents:document_version_page_count_update' +) +link_document_version_multiple_page_count_update = Link( + icon_class_path='mayan.apps.documents.icons.icon_document_version_page_count_update', + text=_('Update page count'), + view='documents:document_version_multiple_page_count_update' +) link_document_version_view = Link( args='resolved_object.pk', icon_class_path='mayan.apps.documents.icons.icon_document_version_view', diff --git a/mayan/apps/documents/literals.py b/mayan/apps/documents/literals.py index d639d1980f..35ef6b2f36 100644 --- a/mayan/apps/documents/literals.py +++ b/mayan/apps/documents/literals.py @@ -43,3 +43,5 @@ PAGE_RANGE_RANGE = 'range' PAGE_RANGE_CHOICES = ( (PAGE_RANGE_ALL, _('All pages')), (PAGE_RANGE_RANGE, _('Page range')) ) + +RETRY_DELAY_DOCUMENT_RESET_PAGES = 30 diff --git a/mayan/apps/documents/managers.py b/mayan/apps/documents/managers.py index 6c9dd92640..28236f109c 100644 --- a/mayan/apps/documents/managers.py +++ b/mayan/apps/documents/managers.py @@ -28,15 +28,15 @@ class DocumentManager(models.Manager): class DocumentPageManager(models.Manager): def get_by_natural_key(self, page_number, document_version_natural_key): - DocumentVersion = apps.get_model( - app_label='documents', model_name='DocumentVersion' + Document = apps.get_model( + app_label='documents', model_name='Document' ) try: - document_version = DocumentVersion.objects.get_by_natural_key(*document_version_natural_key) - except DocumentVersion.DoesNotExist: + document = Document.objects.get_by_natural_key(*document_version_natural_key) + except Document.DoesNotExist: raise self.model.DoesNotExist - return self.get(document_version__pk=document_version.pk, page_number=page_number) + return self.get(document__pk=document.pk, page_number=page_number) def get_queryset(self): return models.QuerySet( @@ -124,6 +124,19 @@ class DocumentVersionManager(models.Manager): return self.get(document__pk=document.pk, checksum=checksum) +class DocumentVersionPageManager(models.Manager): + def get_by_natural_key(self, page_number, document_version_natural_key): + DocumentVersion = apps.get_model( + app_label='documents', model_name='DocumentVersion' + ) + try: + document_version = DocumentVersion.objects.get_by_natural_key(*document_version_natural_key) + except DocumentVersion.DoesNotExist: + raise self.model.DoesNotExist + + return self.get(document_version__pk=document_version.pk, page_number=page_number) + + class DuplicatedDocumentManager(models.Manager): def clean_empty_duplicate_lists(self): self.filter(documents=None).delete() diff --git a/mayan/apps/documents/migrations/0052_rename_document_page.py b/mayan/apps/documents/migrations/0052_rename_document_page.py new file mode 100644 index 0000000000..d6751c3810 --- /dev/null +++ b/mayan/apps/documents/migrations/0052_rename_document_page.py @@ -0,0 +1,38 @@ +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ('documents', '0051_documentpage_enabled'), + ] + + operations = [ + migrations.DeleteModel( + name='DocumentPageResult', + ), + migrations.RenameModel('DocumentPage', 'DocumentVersionPage'), + migrations.AlterField( + model_name='documentversionpage', + name='document_version', + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='pages', to='documents.DocumentVersion', + verbose_name='Document version' + ), + ), + migrations.RemoveField( + model_name='documentversionpage', + name='enabled', + ), + migrations.AlterModelOptions( + name='documentversionpage', + options={ + 'ordering': ('page_number',), + 'verbose_name': 'Document version page', + 'verbose_name_plural': 'Document version pages' + }, + ), + ] diff --git a/mayan/apps/documents/migrations/0053_create_document_page_and_result_models.py b/mayan/apps/documents/migrations/0053_create_document_page_and_result_models.py new file mode 100644 index 0000000000..f7f4081c6e --- /dev/null +++ b/mayan/apps/documents/migrations/0053_create_document_page_and_result_models.py @@ -0,0 +1,57 @@ +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('documents', '0052_rename_document_page'), + ] + + operations = [ + migrations.CreateModel( + name='DocumentPage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), + ('page_number', models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='Page number')), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pages', to='documents.Document', verbose_name='Document')), + ], + options={ + 'unique_together': set([('document', 'page_number')]), + 'verbose_name': 'Document page', + 'verbose_name_plural': 'Document pages', + 'ordering': ('page_number',), + }, + ), + migrations.CreateModel( + name='DocumentPageResult', + fields=[ + ], + options={ + 'verbose_name': 'Document page result', + 'verbose_name_plural': 'Document pages result', + 'ordering': ('document', 'page_number'), + 'proxy': True, + 'indexes': [], + }, + bases=('documents.documentpage',), + ), + migrations.CreateModel( + name='DocumentVersionPageResult', + fields=[ + ], + options={ + 'verbose_name': 'Document version page', + 'verbose_name_plural': 'Document version pages', + 'ordering': ('document_version__document', 'page_number'), + 'proxy': True, + 'indexes': [], + }, + bases=('documents.documentversionpage',), + ), + ] diff --git a/mayan/apps/documents/migrations/0054_reset_document_pages.py b/mayan/apps/documents/migrations/0054_reset_document_pages.py new file mode 100644 index 0000000000..413f423e3d --- /dev/null +++ b/mayan/apps/documents/migrations/0054_reset_document_pages.py @@ -0,0 +1,62 @@ +from __future__ import unicode_literals + +from django.db import migrations + + +def get_latest_version(document): + return document.versions.order_by('timestamp').last() + + +def operation_reset_document_pages(apps, schema_editor): + Document = apps.get_model(app_label='documents', model_name='Document') + DocumentPage = apps.get_model( + app_label='documents', model_name='DocumentPage' + ) + + # Define inside the function to use the migration's apps instance + def pages_reset(document): + ContentType = apps.get_model('contenttypes', 'ContentType') + DocumentVersionPage = apps.get_model( + app_label='documents', model_name='DocumentVersionPage' + ) + + content_type = ContentType.objects.get_for_model( + model=DocumentVersionPage + ) + + for document_page in document.pages.all(): + document_page.delete() + + for version_page in get_latest_version(document=document).pages.all(): + document_page = document.pages.create( + content_type=content_type, + page_number=version_page.page_number, + object_id=version_page.pk, + ) + + for document in Document.objects.using(schema_editor.connection.alias).all(): + pages_reset(document=document) + + +def operation_reset_document_pages_reverse(apps, schema_editor): + Document = apps.get_model(app_label='documents', model_name='Document') + DocumentPage = apps.get_model( + app_label='documents', model_name='DocumentPage' + ) + + for document in Document.objects.using(schema_editor.connection.alias).all(): + for document_page in document.pages.all(): + document_page.delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('documents', '0053_create_document_page_and_result_models'), + ] + + operations = [ + migrations.RunPython( + code=operation_reset_document_pages, + reverse_code=operation_reset_document_pages_reverse + ), + ] diff --git a/mayan/apps/documents/models/__init__.py b/mayan/apps/documents/models/__init__.py index bb4b55b82a..69063382c8 100644 --- a/mayan/apps/documents/models/__init__.py +++ b/mayan/apps/documents/models/__init__.py @@ -2,4 +2,5 @@ from .document_models import * # NOQA from .document_page_models import * # NOQA from .document_type_models import * # NOQA from .document_version_models import * # NOQA +from .document_version_page_models import * # NOQA from .misc_models import * # NOQA diff --git a/mayan/apps/documents/models/document_models.py b/mayan/apps/documents/models/document_models.py index 8b6fef2400..95cbba506d 100644 --- a/mayan/apps/documents/models/document_models.py +++ b/mayan/apps/documents/models/document_models.py @@ -5,9 +5,10 @@ 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.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext, ugettext_lazy as _ @@ -15,6 +16,7 @@ from ..events import ( event_document_create, event_document_properties_edit, event_document_type_change, ) +from ..literals import DOCUMENT_IMAGES_CACHE_NAME from ..managers import DocumentManager, PassthroughManager, TrashCanManager from ..settings import setting_language from ..signals import post_document_type_change @@ -102,6 +104,26 @@ class Document(models.Model): ) return RecentDocument.objects.add_document_for_user(user, self) + @cached_property + def cache(self): + Cache = apps.get_model(app_label='file_caching', model_name='Cache') + return Cache.objects.get(name=DOCUMENT_IMAGES_CACHE_NAME) + + @cached_property + def cache_partition(self): + partition, created = self.cache.partitions.get_or_create( + name='document-{}'.format(self.uuid) + ) + return partition + + @property + def checksum(self): + return self.latest_version.checksum + + @property + def date_updated(self): + return self.latest_version.timestamp + def delete(self, *args, **kwargs): to_trash = kwargs.pop('to_trash', True) @@ -126,25 +148,37 @@ class Document(models.Model): else: return False + @property + def file_mime_encoding(self): + return self.latest_version.encoding + + @property + def file_mimetype(self): + return self.latest_version.mimetype + def get_absolute_url(self): return reverse( viewname='documents:document_preview', kwargs={'pk': self.pk} ) def get_api_image_url(self, *args, **kwargs): - latest_version = self.latest_version - if latest_version: - return latest_version.get_api_image_url(*args, **kwargs) + first_page = self.pages.first() + if first_page: + return first_page.get_api_image_url(*args, **kwargs) @property def is_in_trash(self): return self.in_trash + @property + def latest_version(self): + return self.versions.order_by('timestamp').last() + def natural_key(self): return (self.uuid,) natural_key.dependencies = ['documents.DocumentType'] - def new_version(self, file_object, comment=None, _user=None): + def new_version(self, file_object, append_pages=False, comment=None, _user=None): logger.info('Creating new document version for document: %s', self) DocumentVersion = apps.get_model( app_label='documents', model_name='DocumentVersion' @@ -153,9 +187,10 @@ class Document(models.Model): document_version = DocumentVersion( document=self, comment=comment or '', file=File(file_object) ) - document_version.save(_user=_user) + document_version.save(append_pages=append_pages, _user=_user) logger.info('New document version queued for document: %s', self) + return document_version def open(self, *args, **kwargs): @@ -165,6 +200,34 @@ class Document(models.Model): """ return self.latest_version.open(*args, **kwargs) + @property + def page_count(self): + return self.pages.count() + + @property + def pages(self): + return self.pages.all() + + @property + def pages_all(self): + DocumentPage = apps.get_model( + app_label='documents', model_name='DocumentPage' + ) + return DocumentPage.passthrough.filter(document=self) + + def pages_reset(self, update_page_count=True): + with transaction.atomic(): + for page in self.pages.all(): + page.delete() + + if update_page_count: + self.latest_version.update_page_count() + + for version_page in self.latest_version.pages.all(): + document_page = self.pages.create( + content_object = version_page + ) + def restore(self): self.in_trash = False self.save() @@ -209,53 +272,3 @@ class Document(models.Model): @property def size(self): return self.latest_version.size - - # Compatibility methods - - @property - def checksum(self): - return self.latest_version.checksum - - @property - def date_updated(self): - return self.latest_version.timestamp - - @property - def file_mime_encoding(self): - return self.latest_version.encoding - - @property - def file_mimetype(self): - return self.latest_version.mimetype - - @property - def latest_version(self): - return self.versions.order_by('timestamp').last() - - @property - 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: - return self.latest_version.pages - except AttributeError: - # Document has no version yet - DocumentPage = apps.get_model( - app_label='documents', model_name='DocumentPage' - ) - - return DocumentPage.objects.none() diff --git a/mayan/apps/documents/models/document_page_models.py b/mayan/apps/documents/models/document_page_models.py index 00be66ab4b..b6784a1ff8 100644 --- a/mayan/apps/documents/models/document_page_models.py +++ b/mayan/apps/documents/models/document_page_models.py @@ -4,14 +4,16 @@ import logging from furl import furl +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from django.db import models +from django.db.models import Max from django.urls import reverse from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from mayan.apps.converter.literals import DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION - from mayan.apps.converter.models import LayerTransformation from mayan.apps.converter.transformations import ( BaseTransformation, TransformationResize, TransformationRotate, @@ -26,7 +28,7 @@ from ..settings import ( setting_zoom_min_level ) -from .document_version_models import DocumentVersion +from .document_models import Document __all__ = ('DocumentPage', 'DocumentPageResult') logger = logging.getLogger(__name__) @@ -35,16 +37,22 @@ logger = logging.getLogger(__name__) @python_2_unicode_compatible class DocumentPage(models.Model): """ - Model that describes a document version page + Model that describes a document page """ - document_version = models.ForeignKey( - on_delete=models.CASCADE, related_name='version_pages', to=DocumentVersion, - verbose_name=_('Document version') + document = models.ForeignKey( + on_delete=models.CASCADE, related_name='pages', to=Document, + verbose_name=_('Document') ) enabled = models.BooleanField(default=True, verbose_name=_('Enabled')) page_number = models.PositiveIntegerField( - db_index=True, default=1, editable=False, - verbose_name=_('Page number') + db_index=True, blank=True, null=True, verbose_name=_('Page number') + ) + content_type = models.ForeignKey( + on_delete=models.CASCADE, to=ContentType + ) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey( + ct_field='content_type', fk_field='object_id' ) objects = DocumentPageManager() @@ -52,6 +60,7 @@ class DocumentPage(models.Model): class Meta: ordering = ('page_number',) + unique_together = ('document', 'page_number') verbose_name = _('Document page') verbose_name_plural = _('Document pages') @@ -60,7 +69,7 @@ class DocumentPage(models.Model): @cached_property def cache_partition(self): - partition, created = self.document_version.cache.partitions.get_or_create( + partition, created = self.document.cache.partitions.get_or_create( name=self.uuid ) return partition @@ -69,19 +78,15 @@ class DocumentPage(models.Model): self.cache_partition.delete() super(DocumentPage, self).delete(*args, **kwargs) - def detect_orientation(self): - with self.document_version.open() as file_object: - converter = get_converter_class()( - file_object=file_object, - mime_type=self.document_version.mimetype - ) - return converter.detect_orientation( - page_number=self.page_number - ) - - @property - def document(self): - return self.document_version.document + #def detect_orientation(self): + # with self.document_version.open() as file_object: + # converter = get_converter_class()( + # file_object=file_object, + # mime_type=self.document_version.mimetype + # ) + # return converter.detect_orientation( + # page_number=self.page_number + # ) def generate_image(self, user=None, **kwargs): transformation_list = self.get_combined_transformation_list(user=user, **kwargs) @@ -90,7 +95,7 @@ class DocumentPage(models.Model): # Check is transformed image is available logger.debug('transformations cache filename: %s', combined_cache_filename) - if not setting_disable_transformed_image_cache.value and self.cache_partition.get_file(filename=combined_cache_filename): + if self.cache_partition.get_file(filename=combined_cache_filename): logger.debug( 'transformations cache file "%s" found', combined_cache_filename ) @@ -128,8 +133,7 @@ class DocumentPage(models.Model): final_url.args = kwargs final_url.path = reverse( viewname='rest_api:documentpage-image', kwargs={ - 'pk': self.document.pk, 'version_pk': self.document_version.pk, - 'page_pk': self.pk + 'pk': self.document.pk, 'page_pk': self.pk } ) final_url.args['_hash'] = transformations_hash @@ -190,12 +194,12 @@ class DocumentPage(models.Model): return transformation_list def get_image(self, transformations=None): - cache_filename = 'base_image' + cache_filename = 'document_page' logger.debug('Page cache filename: %s', cache_filename) cache_file = self.cache_partition.get_file(filename=cache_filename) - if not setting_disable_base_image_cache.value and cache_file: + if cache_file: logger.debug('Page cache file "%s" found', cache_filename) with cache_file.open() as file_object: @@ -216,14 +220,25 @@ class DocumentPage(models.Model): logger.debug('Page cache file "%s" not found', cache_filename) try: - with self.document_version.get_intermediate_file() as file_object: + #with self.document_version.get_intermediate_file() as file_object: + #Render or get cached document version page + + #self.content_object.generate_image() + self.content_object.get_image() + cache_filename = 'base_image' + cache_file = self.content_object.cache_partition.get_file(filename=cache_filename) + + with cache_file.open() as file_object: converter = get_converter_class()( file_object=file_object ) - converter.seek_page(page_number=self.page_number - 1) + converter.seek_page(page_number=0) + #self.page_number - 1) page_image = converter.get_page() + cache_filename = 'document_page' + # Since open "wb+" doesn't create files, create it explicitly with self.cache_partition.create_file(filename=cache_filename) as file_object: file_object.write(page_image.getvalue()) @@ -241,28 +256,39 @@ class DocumentPage(models.Model): ) raise + def get_label(self): + return _( + 'Page %(page_number)d out of %(total_pages)d of %(document)s' + ) % { + 'document': force_text(self.document), + 'page_number': self.page_number, + 'total_pages': self.document.pages_all.count() + } + get_label.short_description = _('Label') + @property def is_in_trash(self): return self.document.is_in_trash - def get_label(self): - return _( - 'Page %(page_num)d out of %(total_pages)d of %(document)s' - ) % { - 'document': force_text(self.document), - 'page_num': self.page_number, - 'total_pages': self.document_version.pages_all.count() - } - get_label.short_description = _('Label') - def natural_key(self): - return (self.page_number, self.document_version.natural_key()) - natural_key.dependencies = ['documents.DocumentVersion'] + return (self.page_number, self.document.natural_key()) + natural_key.dependencies = ['documents.Document'] + + def save(self, *args, **kwargs): + if not self.page_number: + last_page_number = DocumentPage.objects.filter( + document=self.document + ).aggregate(Max('page_number'))['page_number__max'] + if last_page_number is not None: + self.page_number = last_page_number + 1 + else: + self.page_number = 1 + super(DocumentPage, self).save(*args, **kwargs) @property def siblings(self): return DocumentPage.objects.filter( - document_version=self.document_version + document=self.document ) @property @@ -271,12 +297,12 @@ class DocumentPage(models.Model): Make cache UUID a mix of version ID and page ID to avoid using stale images """ - return '{}-{}'.format(self.document_version.uuid, self.pk) + return '{}-{}'.format(self.document.uuid, self.pk) class DocumentPageResult(DocumentPage): class Meta: - ordering = ('document_version__document', 'page_number') + ordering = ('document', 'page_number') proxy = True - verbose_name = _('Document page') - verbose_name_plural = _('Document pages') + verbose_name = _('Document page result') + verbose_name_plural = _('Document pages result') diff --git a/mayan/apps/documents/models/document_version_models.py b/mayan/apps/documents/models/document_version_models.py index 1c24fbfeda..d491ca5bd8 100644 --- a/mayan/apps/documents/models/document_version_models.py +++ b/mayan/apps/documents/models/document_version_models.py @@ -246,23 +246,12 @@ 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): - """ - The number of pages that the document posses. - """ - return self.pages.count() + #@property + #def page_count(self): + # """ + # The number of pages that the document posses. + # """ + # return self.pages.count() def revert(self, _user=None): """ @@ -285,6 +274,7 @@ class DocumentVersion(models.Model): Overloaded save method that updates the document version's checksum, mimetype, and page count when created """ + append_pages = kwargs.pop('append_pages', False) user = kwargs.pop('_user', None) new_document_version = not self.pk @@ -304,7 +294,7 @@ class DocumentVersion(models.Model): # Only do this for new documents self.update_checksum(save=False) self.update_mimetype(save=False) - self.save() + self.save(append_pages=append_pages, _user=user) self.update_page_count(save=False) if setting_fix_orientation.value: self.fix_orientation() @@ -337,6 +327,14 @@ class DocumentVersion(models.Model): sender=Document, instance=self.document ) + if append_pages: + for version_page in self.pages.all(): + self.document.pages.create( + content_object = version_page + ) + else: + self.document.pages_reset(update_page_count=False) + def save_to_file(self, file_object): """ Save a copy of the document from the document storage backend @@ -410,7 +408,7 @@ class DocumentVersion(models.Model): pass else: DocumentPage = apps.get_model( - app_label='documents', model_name='DocumentPage' + app_label='documents', model_name='DocumentVersionPage' ) with transaction.atomic(): diff --git a/mayan/apps/documents/models/document_version_page_models.py b/mayan/apps/documents/models/document_version_page_models.py new file mode 100644 index 0000000000..c0c5b7f8f3 --- /dev/null +++ b/mayan/apps/documents/models/document_version_page_models.py @@ -0,0 +1,282 @@ +from __future__ import absolute_import, unicode_literals + +import logging + +from furl import furl + +from django.db import models +from django.urls import reverse +from django.utils.encoding import force_text, python_2_unicode_compatible +from django.utils.functional import cached_property +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.converter.literals import DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION + +from mayan.apps.converter.models import LayerTransformation +from mayan.apps.converter.transformations import ( + BaseTransformation, TransformationResize, TransformationRotate, + TransformationZoom +) +from mayan.apps.converter.utils import get_converter_class + +from ..managers import DocumentVersionPageManager +from ..settings import ( + setting_disable_base_image_cache, setting_disable_transformed_image_cache, + setting_display_width, setting_display_height, setting_zoom_max_level, + setting_zoom_min_level +) + +from .document_version_models import DocumentVersion + +__all__ = ('DocumentVersionPage', 'DocumentVersionPageResult') +logger = logging.getLogger(__name__) + + +@python_2_unicode_compatible +class DocumentVersionPage(models.Model): + """ + Model that describes a document version page + """ + document_version = models.ForeignKey( + on_delete=models.CASCADE, related_name='pages', to=DocumentVersion, + verbose_name=_('Document version') + ) + page_number = models.PositiveIntegerField( + db_index=True, default=1, editable=False, + verbose_name=_('Page number') + ) + + objects = DocumentVersionPageManager() + + class Meta: + ordering = ('page_number',) + verbose_name = _('Document version page') + verbose_name_plural = _('Document version pages') + + def __str__(self): + return self.get_label() + + @cached_property + def cache_partition(self): + partition, created = self.document_version.cache.partitions.get_or_create( + name=self.uuid + ) + return partition + + def delete(self, *args, **kwargs): + self.cache_partition.delete() + super(DocumentVersionPage, self).delete(*args, **kwargs) + + #def detect_orientation(self): + # with self.document_version.open() as file_object: + # converter = get_converter_class()( + # file_object=file_object, + # mime_type=self.document_version.mimetype + # ) + # return converter.detect_orientation( + # page_number=self.page_number + # ) + + @property + def document(self): + return self.document_version.document + + def generate_image(self, user=None, **kwargs): + transformation_list = self.get_combined_transformation_list(user=user, **kwargs) + combined_cache_filename = BaseTransformation.combine(transformation_list) + + # Check is transformed image is available + logger.debug('transformations cache filename: %s', combined_cache_filename) + + if self.cache_partition.get_file(filename=combined_cache_filename): + logger.debug( + 'transformations cache file "%s" found', combined_cache_filename + ) + else: + logger.debug( + 'transformations cache file "%s" not found', combined_cache_filename + ) + image = self.get_image(transformations=transformation_list) + with self.cache_partition.create_file(filename=combined_cache_filename) as file_object: + file_object.write(image.getvalue()) + + return combined_cache_filename + + #def get_absolute_url(self): + # return reverse( + # viewname='documents:document_version_page_view', kwargs={ + # 'pk': self.pk + # } + # ) + + def get_api_image_url(self, *args, **kwargs): + """ + Create an unique URL combining: + - the page's image URL + - the interactive argument + - a hash from the server side and interactive transformations + The purpose of this unique URL is to allow client side caching + if document page images. + """ + transformations_hash = BaseTransformation.combine( + self.get_combined_transformation_list(*args, **kwargs) + ) + + kwargs.pop('transformations', None) + + final_url = furl() + final_url.args = kwargs + final_url.path = reverse( + viewname='rest_api:documentversionpage-image', kwargs={ + 'pk': self.document.pk, 'version_pk': self.document_version.pk, + 'page_pk': self.pk + } + ) + final_url.args['_hash'] = transformations_hash + + return final_url.tostr() + + def get_combined_transformation_list(self, user=None, *args, **kwargs): + """ + Return a list of transformation containing the server side + document page transformation as well as tranformations created + from the arguments as transient interactive transformation. + """ + # Convert arguments into transformations + transformations = kwargs.get('transformations', []) + + # Set sensible defaults if the argument is not specified or if the + # argument is None + width = kwargs.get('width', setting_display_width.value) or setting_display_width.value + height = kwargs.get('height', setting_display_height.value) or setting_display_height.value + rotation = kwargs.get('rotation', DEFAULT_ROTATION) or DEFAULT_ROTATION + zoom_level = kwargs.get('zoom', DEFAULT_ZOOM_LEVEL) or DEFAULT_ZOOM_LEVEL + + if zoom_level < setting_zoom_min_level.value: + zoom_level = setting_zoom_min_level.value + + if zoom_level > setting_zoom_max_level.value: + zoom_level = setting_zoom_max_level.value + + # Generate transformation hash + transformation_list = [] + + maximum_layer_order = kwargs.get('maximum_layer_order', None) + + # Stored transformations first + for stored_transformation in LayerTransformation.objects.get_for_object( + self, maximum_layer_order=maximum_layer_order, as_classes=True, + user=user + ): + transformation_list.append(stored_transformation) + + # Interactive transformations second + for transformation in transformations: + transformation_list.append(transformation) + + if rotation: + transformation_list.append( + TransformationRotate(degrees=rotation) + ) + + if width: + transformation_list.append( + TransformationResize(width=width, height=height) + ) + + if zoom_level: + transformation_list.append(TransformationZoom(percent=zoom_level)) + + return transformation_list + + def get_image(self, transformations=None): + cache_filename = 'base_image' + logger.debug('Page cache filename: %s', cache_filename) + + cache_file = self.cache_partition.get_file(filename=cache_filename) + + if cache_file: + logger.debug('Page cache file "%s" found', cache_filename) + + with cache_file.open() as file_object: + converter = get_converter_class()( + file_object=file_object + ) + + converter.seek_page(page_number=0) + + # This code is also repeated below to allow using a context + # manager with cache_file.open and close it automatically. + # Apply runtime transformations + for transformation in transformations or []: + converter.transform(transformation=transformation) + + return converter.get_page() + else: + logger.debug('Page cache file "%s" not found', cache_filename) + + try: + with self.document_version.get_intermediate_file() as file_object: + converter = get_converter_class()( + file_object=file_object + ) + converter.seek_page(page_number=self.page_number - 1) + + page_image = converter.get_page() + + # Since open "wb+" doesn't create files, create it explicitly + with self.cache_partition.create_file(filename=cache_filename) as file_object: + file_object.write(page_image.getvalue()) + + # Apply runtime transformations + for transformation in transformations or []: + converter.transform(transformation=transformation) + + return converter.get_page() + except Exception as exception: + # Cleanup in case of error + logger.error( + 'Error creating page cache file "%s"; %s', + cache_filename, exception + ) + raise + + #@property + #def is_in_trash(self): + # return self.document.is_in_trash + + def get_label(self): + return _( + 'Version page %(page_number)d out of %(total_pages)d of %(document)s' + ) % { + 'document': force_text(self.document), + 'page_number': self.page_number, + 'total_pages': self.document_version.pages.count() + } + get_label.short_description = _('Label') + + def natural_key(self): + return (self.page_number, self.document_version.natural_key()) + natural_key.dependencies = ['documents.DocumentVersion'] + + @property + def siblings(self): + return DocumentVersionPage.objects.filter( + document_version=self.document_version + ) + + @property + def uuid(self): + """ + Make cache UUID a mix of version ID and page ID to avoid using stale + images + """ + return '{}-{}'.format(self.document_version.uuid, self.pk) + + +class DocumentVersionPageResult(DocumentVersionPage): + class Meta: + ordering = ('document_version__document', 'page_number') + proxy = True + verbose_name = _('Document version page') + verbose_name_plural = _('Document version pages') diff --git a/mayan/apps/documents/queues.py b/mayan/apps/documents/queues.py index 11280f17d8..e01c62d8e3 100644 --- a/mayan/apps/documents/queues.py +++ b/mayan/apps/documents/queues.py @@ -30,6 +30,10 @@ queue_converter.add_task_type( dotted_path='mayan.apps.documents.tasks.task_generate_document_page_image', label=_('Generate document page image') ) +queue_converter.add_task_type( + dotted_path='mayan.apps.documents.tasks.task_generate_document_version_page_image', + label=_('Generate document version page image') +) queue_documents.add_task_type( dotted_path='mayan.apps.documents.tasks.task_delete_document', @@ -66,6 +70,10 @@ queue_tools.add_task_type( label=_('Duplicated document scan') ) +queue_uploads.add_task_type( + dotted_path='mayan.apps.documents.tasks.task_document_pages_reset', + label=_('Reset document pages') +) queue_uploads.add_task_type( dotted_path='mayan.apps.documents.tasks.task_update_page_count', label=_('Update document page count') diff --git a/mayan/apps/documents/search.py b/mayan/apps/documents/search.py index de03a36dd0..e631099d3c 100644 --- a/mayan/apps/documents/search.py +++ b/mayan/apps/documents/search.py @@ -17,12 +17,20 @@ def transformation_format_uuid(term_string): return term_string -def get_queryset_page_search_queryset(): +def get_queryset_document_page_search_queryset(): # Ignore documents in trash can DocumentPage = apps.get_model( app_label='documents', model_name='DocumentPage' ) - return DocumentPage.objects.filter(document_version__document__in_trash=False) + return DocumentPage.objects.filter(document__in_trash=False) + + +def get_queryset_document_version_page_search_queryset(): + # Ignore documents in trash can + DocumentVersionPage = apps.get_model( + app_label='documents', model_name='DocumentVersionPage' + ) + return DocumentVersionPage.objects.filter(document_version__document__in_trash=False) document_search = SearchModel( @@ -30,7 +38,6 @@ document_search = SearchModel( model_name='Document', permission=permission_document_view, serializer_path='mayan.apps.documents.serializers.DocumentSerializer' ) - document_search.add_model_field( field='document_type__label', label=_('Document type') ) @@ -50,24 +57,49 @@ document_search.add_model_field( document_page_search = SearchModel( app_label='documents', list_mode=LIST_MODE_CHOICE_ITEM, model_name='DocumentPage', permission=permission_document_view, - queryset=get_queryset_page_search_queryset, + queryset=get_queryset_document_page_search_queryset, serializer_path='mayan.apps.documents.serializers.DocumentPageSerializer' ) +document_version_page_search = SearchModel( + app_label='documents', list_mode=LIST_MODE_CHOICE_ITEM, + model_name='DocumentVersionPage', permission=permission_document_view, + queryset=get_queryset_document_version_page_search_queryset, + serializer_path='mayan.apps.documents.serializers.DocumentVersionPageSerializer' +) + document_page_search.add_model_field( - field='document_version__document__document_type__label', + field='document__document_type__label', label=_('Document type') ) document_page_search.add_model_field( - field='document_version__document__versions__mimetype', + field='document__versions__mimetype', label=_('MIME type') ) document_page_search.add_model_field( + field='document__label', label=_('Label') +) +document_page_search.add_model_field( + field='document__description', label=_('Description') +) +document_page_search.add_model_field( + field='document__versions__checksum', label=_('Checksum') +) + +document_version_page_search.add_model_field( + field='document_version__document__document_type__label', + label=_('Document type') +) +document_version_page_search.add_model_field( + field='document_version__document__versions__mimetype', + label=_('MIME type') +) +document_version_page_search.add_model_field( field='document_version__document__label', label=_('Label') ) -document_page_search.add_model_field( +document_version_page_search.add_model_field( field='document_version__document__description', label=_('Description') ) -document_page_search.add_model_field( +document_version_page_search.add_model_field( field='document_version__checksum', label=_('Checksum') ) diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py index 9632a546cb..5a6c172f7a 100644 --- a/mayan/apps/documents/serializers.py +++ b/mayan/apps/documents/serializers.py @@ -8,42 +8,40 @@ from rest_framework.reverse import reverse from mayan.apps.common.models import SharedUploadedFile from .models import ( - Document, DocumentVersion, DocumentPage, DocumentType, - DocumentTypeFilename, RecentDocument + Document, DocumentPage, DocumentType, DocumentTypeFilename, + DocumentVersion, DocumentVersionPage, RecentDocument ) from .settings import setting_language from .tasks import task_upload_new_version class DocumentPageSerializer(serializers.HyperlinkedModelSerializer): - document_version_url = serializers.SerializerMethodField() + document_url = serializers.SerializerMethodField() image_url = serializers.SerializerMethodField() url = serializers.SerializerMethodField() class Meta: - fields = ('document_version_url', 'image_url', 'page_number', 'url') + fields = ('document_url', 'image_url', 'page_number', 'url') model = DocumentPage - def get_document_version_url(self, instance): + def get_document_url(self, instance): return reverse( - viewname='rest_api:documentversion-detail', args=( - instance.document.pk, instance.document_version.pk, + viewname='rest_api:document-detail', args=( + instance.document.pk, ), request=self.context['request'], format=self.context['format'] ) def get_image_url(self, instance): return reverse( viewname='rest_api:documentpage-image', args=( - instance.document.pk, instance.document_version.pk, - instance.pk, + instance.document.pk, instance.pk, ), request=self.context['request'], format=self.context['format'] ) def get_url(self, instance): return reverse( viewname='rest_api:documentpage-detail', args=( - instance.document.pk, instance.document_version.pk, - instance.pk, + instance.document.pk, instance.pk, ), request=self.context['request'], format=self.context['format'] ) @@ -97,6 +95,39 @@ class WritableDocumentTypeSerializer(serializers.ModelSerializer): return obj.documents.count() +class DocumentVersionPageSerializer(serializers.HyperlinkedModelSerializer): + document_version_url = serializers.SerializerMethodField() + image_url = serializers.SerializerMethodField() + url = serializers.SerializerMethodField() + + class Meta: + fields = ('document_version_url', 'image_url', 'page_number', 'url') + model = DocumentVersionPage + + def get_document_version_url(self, instance): + return reverse( + viewname='rest_api:documentversion-detail', args=( + instance.document.pk, instance.document_version.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + def get_image_url(self, instance): + return reverse( + viewname='rest_api:documentversionpage-image', args=( + instance.document.pk, instance.document_version.pk, + instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + def get_url(self, instance): + return reverse( + viewname='rest_api:documentversionpage-detail', args=( + instance.document.pk, instance.document_version.pk, + instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer): document_url = serializers.SerializerMethodField() download_url = serializers.SerializerMethodField() diff --git a/mayan/apps/documents/statistics.py b/mayan/apps/documents/statistics.py index 2f1059e7d1..94d91be994 100644 --- a/mayan/apps/documents/statistics.py +++ b/mayan/apps/documents/statistics.py @@ -41,7 +41,7 @@ def new_documents_per_month(): def new_document_pages_per_month(): DocumentPage = apps.get_model( - app_label='documents', model_name='DocumentPage' + app_label='documents', model_name='DocumentVersionPage' ) qss = qsstats.QuerySetStats( @@ -106,7 +106,7 @@ def new_document_pages_this_month(user=None): app_label='acls', model_name='AccessControlList' ) DocumentPage = apps.get_model( - app_label='documents', model_name='DocumentPage' + app_label='documents', model_name='DocumentVersionPage' ) queryset = DocumentPage.objects.all() @@ -195,7 +195,7 @@ def total_document_version_per_month(): def total_document_page_per_month(): DocumentPage = apps.get_model( - app_label='documents', model_name='DocumentPage' + app_label='documents', model_name='DocumentVersionPage' ) qss = qsstats.QuerySetStats( diff --git a/mayan/apps/documents/tasks.py b/mayan/apps/documents/tasks.py index 34956086bc..a2d4fe9fc7 100644 --- a/mayan/apps/documents/tasks.py +++ b/mayan/apps/documents/tasks.py @@ -9,8 +9,8 @@ from django.db import OperationalError from mayan.celery import app from .literals import ( - UPDATE_PAGE_COUNT_RETRY_DELAY, UPLOAD_NEW_DOCUMENT_RETRY_DELAY, - UPLOAD_NEW_VERSION_RETRY_DELAY + RETRY_DELAY_DOCUMENT_RESET_PAGES, UPDATE_PAGE_COUNT_RETRY_DELAY, + UPLOAD_NEW_DOCUMENT_RETRY_DELAY, ) logger = logging.getLogger(__name__) @@ -65,6 +65,25 @@ def task_delete_stubs(): logger.info(msg='Finshed') +@app.task(bind=True, default_retry_delay=RETRY_DELAY_DOCUMENT_RESET_PAGES, ignore_result=True) +def task_document_pages_reset(self, document_id): + Document = apps.get_model( + app_label='documents', model_name='Document' + ) + + document = Document.objects.get(pk=document_id) + + try: + document.pages_reset() + except OperationalError as exception: + logger.warning( + 'Operational error during attempt to reset pages for ' + 'document: %s; %s. Retrying.', document, + exception + ) + raise self.retry(exc=exception) + + @app.task() def task_generate_document_page_image(document_page_id, user_id=None, **kwargs): DocumentPage = apps.get_model( @@ -81,6 +100,22 @@ def task_generate_document_page_image(document_page_id, user_id=None, **kwargs): return document_page.generate_image(user=user, **kwargs) +@app.task() +def task_generate_document_version_page_image(document_version_page_id, user_id=None, **kwargs): + DocumentVersionPage = apps.get_model( + app_label='documents', model_name='DocumentVersionPage' + ) + User = get_user_model() + + if user_id: + user = User.objects.get(pk=user_id) + else: + user = None + + document_version_page = DocumentVersionPage.objects.get(pk=document_version_page_id) + return document_version_page.generate_image(user=user, **kwargs) + + @app.task(ignore_result=True) def task_scan_duplicates_all(): DuplicatedDocument = apps.get_model( @@ -177,7 +212,7 @@ def task_upload_new_document(self, document_type_id, shared_uploaded_file_id): @app.task(bind=True, default_retry_delay=UPLOAD_NEW_VERSION_RETRY_DELAY, ignore_result=True) -def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id, comment=None): +def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id, append_pages=False, comment=None): SharedUploadedFile = apps.get_model( app_label='common', model_name='SharedUploadedFile' ) @@ -212,7 +247,7 @@ def task_upload_new_version(self, document_id, shared_uploaded_file_id, user_id, document=document, comment=comment or '', file=file_object ) try: - document_version.save(_user=user) + document_version.save(append_pages=append_pages, _user=user) except Warning as warning: # New document version are blocked logger.info( diff --git a/mayan/apps/documents/tests/mixins.py b/mayan/apps/documents/tests/mixins.py index bb996c6cde..d11afaa921 100644 --- a/mayan/apps/documents/tests/mixins.py +++ b/mayan/apps/documents/tests/mixins.py @@ -69,6 +69,8 @@ class DocumentTestMixin(object): self.test_document = document self.test_documents.append(document) + self.test_document_version = document.latest_version + self.test_document_page = document.pages_all.first() class DocumentTypeViewTestMixin(object): @@ -148,6 +150,26 @@ class DocumentVersionTestMixin(object): ) +class DocumentVersionViewTestMixin(object): + def _request_document_version_list_view(self): + return self.get( + viewname='documents:document_version_list', + kwargs={'pk': self.test_document.pk} + ) + + def _request_document_version_revert_view(self, document_version): + return self.post( + viewname='documents:document_version_revert', + kwargs={'pk': document_version.pk} + ) + + def _request_test_document_version_page_count_update_view(self): + return self.post( + viewname='documents:document_version_page_count_update', + kwargs={'pk': self.test_document_version.pk} + ) + + class DocumentViewTestMixin(object): def _request_document_properties_view(self): return self.get( @@ -200,6 +222,12 @@ class DocumentViewTestMixin(object): data={'id_list': self.test_document.pk} ) + def _request_document_pages_reset_view(self): + return self.post( + viewname='documents:document_pages_reset', + kwargs={'pk': self.test_document.pk} + ) + def _request_document_version_download(self, data=None): data = data or {} return self.get( @@ -208,18 +236,6 @@ class DocumentViewTestMixin(object): }, data=data ) - def _request_document_update_page_count_view(self): - return self.post( - viewname='documents:document_update_page_count', - kwargs={'pk': self.test_document.pk} - ) - - def _request_document_multiple_update_page_count_view(self): - return self.post( - viewname='documents:document_multiple_update_page_count', - data={'id_list': self.test_document.pk} - ) - def _request_document_clear_transformations_view(self): return self.post( viewname='documents:document_clear_transformations', @@ -232,8 +248,11 @@ class DocumentViewTestMixin(object): data={'id_list': self.test_document.pk} ) - def _request_empty_trash_view(self): - return self.post(viewname='documents:trash_can_empty') + def _request_document_multiple_pages_reset_view(self): + return self.post( + viewname='documents:document_multiple_pages_reset', + data={'id_list': self.test_document.pk} + ) def _request_document_print_view(self): return self.get( @@ -243,3 +262,6 @@ class DocumentViewTestMixin(object): 'page_group': PAGE_RANGE_ALL } ) + + def _request_empty_trash_view(self): + return self.post(viewname='documents:trash_can_empty') diff --git a/mayan/apps/documents/tests/test_api.py b/mayan/apps/documents/tests/test_api.py index dea50b2618..60694de79e 100644 --- a/mayan/apps/documents/tests/test_api.py +++ b/mayan/apps/documents/tests/test_api.py @@ -530,8 +530,7 @@ class DocumentPageAPIViewTestMixin(object): page = self.test_document.pages.first() return self.get( viewname='rest_api:documentpage-image', kwargs={ - 'pk': page.document.pk, 'version_pk': page.document_version.pk, - 'page_pk': page.pk + 'pk': page.document.pk, 'page_pk': page.pk } ) @@ -552,6 +551,33 @@ class DocumentPageAPIViewTestCase( self.assertEqual(response.status_code, status.HTTP_200_OK) +class DocumentVersionPageAPIViewTestMixin(object): + def _request_document_version_page_image(self): + page = self.test_document_version.pages.first() + return self.get( + viewname='rest_api:documentversionpage-image', kwargs={ + 'pk': page.document.pk, 'version_pk': page.document_version.pk, + 'page_pk': page.pk + } + ) + + +class DocumentVersionPageAPIViewTestCase( + DocumentVersionPageAPIViewTestMixin, DocumentTestMixin, BaseAPITestCase +): + def test_document_version_page_api_image_view_no_access(self): + response = self._request_document_version_page_image() + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_document_version_page_api_image_view_with_access(self): + self.grant_access( + obj=self.test_document, permission=permission_document_view + ) + + response = self._request_document_version_page_image() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + class TrashedDocumentAPIViewTestMixin(object): def _request_test_document_api_trash_view(self): return self.delete( @@ -575,13 +601,10 @@ class TrashedDocumentAPIViewTestMixin(object): ) def _request_test_trashed_document_api_image_view(self): - latest_version = self.test_document.latest_version - return self.get( viewname='rest_api:documentpage-image', kwargs={ - 'pk': latest_version.document.pk, - 'version_pk': latest_version.pk, - 'page_pk': latest_version.pages.first().pk + 'pk': self.test_document.pk, + 'page_pk': self.test_document.pages.first().pk } ) diff --git a/mayan/apps/documents/tests/test_document_page_views.py b/mayan/apps/documents/tests/test_document_page_views.py index 29ee28cec3..96b2702968 100644 --- a/mayan/apps/documents/tests/test_document_page_views.py +++ b/mayan/apps/documents/tests/test_document_page_views.py @@ -9,10 +9,10 @@ from ..permissions import ( from .base import GenericDocumentViewTestCase -class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase): - def setUp(self): - super(DocumentPageDisableViewTestCase, self).setUp() - self.test_document_page = self.test_document.pages_all.first() +class DocumentPageDisableViewTestMixin(object): + def _disable_test_document_page(self): + self.test_document_page.enabled = False + self.test_document_page.save() def _request_test_document_page_disable_view(self): return self.post( @@ -21,6 +21,31 @@ class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase): } ) + def _request_test_document_page_enable_view(self): + return self.post( + viewname='documents:document_page_enable', kwargs={ + 'pk': self.test_document_page.pk + } + ) + + 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 _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 + } + ) + + +class DocumentPageDisableViewTestCase( + DocumentPageDisableViewTestMixin, GenericDocumentViewTestCase +): def test_document_page_disable_view_no_permission(self): test_document_page_count = self.test_document.pages.count() @@ -45,13 +70,6 @@ class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase): 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() @@ -76,17 +94,6 @@ class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase): 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() @@ -114,13 +121,6 @@ class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase): 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() @@ -148,7 +148,7 @@ class DocumentPageDisableViewTestCase(GenericDocumentViewTestCase): ) -class DocumentPageViewTestCase(GenericDocumentViewTestCase): +class DocumentPageViewTestMixin(object): def _request_test_document_page_list_view(self): return self.get( viewname='documents:document_pages', kwargs={ @@ -156,6 +156,18 @@ class DocumentPageViewTestCase(GenericDocumentViewTestCase): } ) + def _request_test_document_page_view(self, document_page): + return self.get( + viewname='documents:document_page_view', kwargs={ + 'pk': document_page.pk, + } + ) + + +class DocumentPageViewTestCase( + DocumentPageViewTestMixin, GenericDocumentViewTestCase +): + def test_document_page_list_view_no_permission(self): response = self._request_test_document_page_list_view() self.assertEqual(response.status_code, 404) @@ -170,13 +182,6 @@ class DocumentPageViewTestCase(GenericDocumentViewTestCase): response=response, text=self.test_document.label, status_code=200 ) - def _request_test_document_page_view(self, document_page): - return self.get( - viewname='documents:document_page_view', kwargs={ - 'pk': document_page.pk, - } - ) - def test_document_page_view_no_permissions(self): response = self._request_test_document_page_view( document_page=self.test_document.pages.first() diff --git a/mayan/apps/documents/tests/test_document_version_views.py b/mayan/apps/documents/tests/test_document_version_views.py index 4f712a638b..d2ab419378 100644 --- a/mayan/apps/documents/tests/test_document_version_views.py +++ b/mayan/apps/documents/tests/test_document_version_views.py @@ -1,21 +1,19 @@ from __future__ import unicode_literals from ..permissions import ( - permission_document_version_revert, permission_document_version_view, + permission_document_tools, permission_document_version_revert, + permission_document_version_view, ) from .base import GenericDocumentViewTestCase from .literals import TEST_VERSION_COMMENT -from .mixins import DocumentVersionTestMixin +from .mixins import DocumentVersionTestMixin, DocumentVersionViewTestMixin -class DocumentVersionTestCase(DocumentVersionTestMixin, GenericDocumentViewTestCase): - def _request_document_version_list_view(self): - return self.get( - viewname='documents:document_version_list', - kwargs={'pk': self.test_document.pk} - ) - +class DocumentVersionTestCase( + DocumentVersionViewTestMixin, DocumentVersionTestMixin, + GenericDocumentViewTestCase +): def test_document_version_list_no_permission(self): self._upload_new_version() @@ -33,12 +31,6 @@ class DocumentVersionTestCase(DocumentVersionTestMixin, GenericDocumentViewTestC response=response, text=TEST_VERSION_COMMENT, status_code=200 ) - def _request_document_version_revert_view(self, document_version): - return self.post( - viewname='documents:document_version_revert', - kwargs={'pk': document_version.pk} - ) - def test_document_version_revert_no_permission(self): first_version = self.test_document.latest_version self._upload_new_version() @@ -64,3 +56,25 @@ class DocumentVersionTestCase(DocumentVersionTestMixin, GenericDocumentViewTestC self.assertEqual(response.status_code, 302) self.assertEqual(self.test_document.versions.count(), 1) + + def test_document_version_page_count_update_view_no_permission(self): + self.test_document_version.pages.all().delete() + + response = self._request_test_document_version_page_count_update_view() + self.assertEqual(response.status_code, 404) + + self.assertEqual(self.test_document_version.pages.count(), 0) + + def test_document_version_page_count_update_view_with_access(self): + page_count = self.test_document_version.pages.count() + + self.test_document_version.pages.all().delete() + + self.grant_access( + obj=self.test_document, permission=permission_document_tools + ) + + response = self._request_test_document_version_page_count_update_view() + self.assertEqual(response.status_code, 302) + + self.assertEqual(self.test_document_version.pages.count(), page_count) diff --git a/mayan/apps/documents/tests/test_document_views.py b/mayan/apps/documents/tests/test_document_views.py index 381a71211d..45c96f6786 100644 --- a/mayan/apps/documents/tests/test_document_views.py +++ b/mayan/apps/documents/tests/test_document_views.py @@ -292,46 +292,44 @@ class DocumentsViewsTestCase( ) ) - def test_document_update_page_count_view_no_permission(self): + def test_document_pages_reset_view_no_permission(self): self.test_document.pages.all().delete() - self.assertEqual(self.test_document.pages.count(), 0) - response = self._request_document_update_page_count_view() + response = self._request_document_pages_reset_view() self.assertEqual(response.status_code, 404) self.assertEqual(self.test_document.pages.count(), 0) - def test_document_update_page_count_view_with_permission(self): - # TODO: Revise permission association - + def test_document_pages_reset_view_with_access(self): page_count = self.test_document.pages.count() self.test_document.pages.all().delete() - self.assertEqual(self.test_document.pages.count(), 0) - self.grant_permission(permission=permission_document_tools) + self.grant_access( + obj=self.test_document, permission=permission_document_tools + ) - response = self._request_document_update_page_count_view() + response = self._request_document_pages_reset_view() self.assertEqual(response.status_code, 302) self.assertEqual(self.test_document.pages.count(), page_count) - def test_document_multiple_update_page_count_view_no_permission(self): + def test_document_multiple_pages_reset_view_no_permission(self): self.test_document.pages.all().delete() - self.assertEqual(self.test_document.pages.count(), 0) - response = self._request_document_multiple_update_page_count_view() + response = self._request_document_multiple_pages_reset_view() self.assertEqual(response.status_code, 404) self.assertEqual(self.test_document.pages.count(), 0) - def test_document_multiple_update_page_count_view_with_permission(self): + def test_document_multiple_pages_reset_view_with_access(self): page_count = self.test_document.pages.count() self.test_document.pages.all().delete() - self.assertEqual(self.test_document.pages.count(), 0) - self.grant_permission(permission=permission_document_tools) + self.grant_access( + obj=self.test_document, permission=permission_document_tools + ) - response = self._request_document_multiple_update_page_count_view() + response = self._request_document_multiple_pages_reset_view() self.assertEqual(response.status_code, 302) self.assertEqual(self.test_document.pages.count(), page_count) diff --git a/mayan/apps/documents/tests/test_search.py b/mayan/apps/documents/tests/test_search.py index 531f6e8d2f..1db040b174 100644 --- a/mayan/apps/documents/tests/test_search.py +++ b/mayan/apps/documents/tests/test_search.py @@ -1,22 +1,30 @@ from __future__ import unicode_literals from mayan.apps.common.tests.base import BaseTestCase -from mayan.apps.documents.permissions import permission_document_view -from mayan.apps.documents.search import document_search, document_page_search -from mayan.apps.documents.tests.mixins import DocumentTestMixin + +from ..permissions import permission_document_view +from ..search import document_search, document_page_search + +from .mixins import DocumentTestMixin -class DocumentSearchTestCase(DocumentTestMixin, BaseTestCase): +class DocumentSearchTestMixin(object): def _perform_document_page_search(self): return document_page_search.search( - query_string={'q': self.test_document.label}, user=self._test_case_user + query_string={'q': self.test_document.label}, + user=self._test_case_user ) def _perform_document_search(self): return document_search.search( - query_string={'q': self.test_document.label}, user=self._test_case_user + query_string={'q': self.test_document.label}, + user=self._test_case_user ) + +class DocumentSearchTestCase( + DocumentSearchTestMixin, DocumentTestMixin, BaseTestCase +): def test_document_page_search_no_access(self): queryset = self._perform_document_page_search() self.assertFalse(self.test_document.pages.first() in queryset) diff --git a/mayan/apps/documents/tests/test_trashed_document_views.py b/mayan/apps/documents/tests/test_trashed_document_views.py index 8be806f36e..775bef0f38 100644 --- a/mayan/apps/documents/tests/test_trashed_document_views.py +++ b/mayan/apps/documents/tests/test_trashed_document_views.py @@ -9,7 +9,7 @@ from ..permissions import ( from .base import GenericDocumentViewTestCase -class TrashedDocumentTestCase(GenericDocumentViewTestCase): +class TrashedDocumentTestMixin(object): def _request_document_restore_get_view(self): return self.get( viewname='documents:document_restore', kwargs={ @@ -17,6 +17,48 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase): } ) + def _request_document_restore_post_view(self): + return self.post( + viewname='documents:document_restore', kwargs={ + 'pk': self.test_document.pk + } + ) + + 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_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 +): def test_document_restore_get_view_no_permission(self): self.test_document.delete() self.assertEqual(Document.objects.count(), 0) @@ -43,13 +85,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase): self.assertEqual(Document.objects.count(), document_count) - def _request_document_restore_post_view(self): - return self.post( - viewname='documents:document_restore', kwargs={ - 'pk': self.test_document.pk - } - ) - def test_document_restore_post_view_no_permission(self): self.test_document.delete() self.assertEqual(Document.objects.count(), 0) @@ -74,13 +109,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase): self.assertEqual(DeletedDocument.objects.count(), 0) self.assertEqual(Document.objects.count(), 1) - def _request_document_trash_get_view(self): - return self.get( - viewname='documents:document_trash', kwargs={ - 'pk': self.test_document.pk - } - ) - def test_document_trash_get_view_no_permissions(self): document_count = Document.objects.count() @@ -101,13 +129,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase): self.assertEqual(Document.objects.count(), document_count) - def _request_document_trash_post_view(self): - return self.post( - viewname='documents:document_trash', kwargs={ - 'pk': self.test_document.pk - } - ) - def test_document_trash_post_view_no_permissions(self): response = self._request_document_trash_post_view() self.assertEqual(response.status_code, 404) @@ -126,13 +147,6 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase): self.assertEqual(DeletedDocument.objects.count(), 1) self.assertEqual(Document.objects.count(), 0) - def _request_document_delete_get_view(self): - return self.get( - viewname='documents:document_delete', kwargs={ - 'pk': self.test_document.pk - } - ) - def test_document_delete_get_view_no_permissions(self): self.test_document.delete() self.assertEqual(Document.objects.count(), 0) @@ -140,7 +154,7 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase): trashed_document_count = DeletedDocument.objects.count() - response = self._request_document_delete_get_view() + response = self._request_trashed_document_delete_get_view() self.assertEqual(response.status_code, 404) self.assertEqual( @@ -158,26 +172,19 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase): trashed_document_count = DeletedDocument.objects.count() - response = self._request_document_delete_get_view() + response = self._request_trashed_document_delete_get_view() self.assertEqual(response.status_code, 200) self.assertEqual( DeletedDocument.objects.count(), trashed_document_count ) - def _request_document_delete_post_view(self): - return self.post( - viewname='documents:document_delete', kwargs={ - 'pk': self.test_document.pk - } - ) - def test_document_delete_post_view_no_permissions(self): self.test_document.delete() self.assertEqual(Document.objects.count(), 0) self.assertEqual(DeletedDocument.objects.count(), 1) - response = self._request_document_delete_post_view() + response = self._request_trashed_document_delete_post_view() self.assertEqual(response.status_code, 404) self.assertEqual(Document.objects.count(), 0) @@ -192,19 +199,16 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase): obj=self.test_document, permission=permission_document_delete ) - response = self._request_document_delete_post_view() + response = self._request_trashed_document_delete_post_view() self.assertEqual(response.status_code, 302) self.assertEqual(DeletedDocument.objects.count(), 0) self.assertEqual(Document.objects.count(), 0) - def _request_document_list_deleted_view(self): - return self.get(viewname='documents:document_list_deleted') - def test_deleted_document_list_view_no_permissions(self): self.test_document.delete() - response = self._request_document_list_deleted_view() + response = self._request_trashed_document_list_view() self.assertNotContains( response=response, text=self.test_document.label, status_code=200 ) @@ -216,7 +220,7 @@ class TrashedDocumentTestCase(GenericDocumentViewTestCase): obj=self.test_document, permission=permission_document_view ) - response = self._request_document_list_deleted_view() + response = self._request_trashed_document_list_view() self.assertContains( response=response, text=self.test_document.label, status_code=200 ) diff --git a/mayan/apps/documents/urls.py b/mayan/apps/documents/urls.py index 84aec25094..a0e25cc670 100644 --- a/mayan/apps/documents/urls.py +++ b/mayan/apps/documents/urls.py @@ -4,21 +4,24 @@ from django.conf.urls import url from .api_views import ( APITrashedDocumentListView, APIDeletedDocumentRestoreView, - APIDeletedDocumentView, APIDocumentDownloadView, APIDocumentView, - APIDocumentListView, APIDocumentVersionDownloadView, + APIDeletedDocumentView, APIDocumentDownloadView, APIDocumentPageListView, + APIDocumentView, APIDocumentListView, APIDocumentVersionDownloadView, APIDocumentPageImageView, APIDocumentPageView, APIDocumentTypeDocumentListView, APIDocumentTypeListView, APIDocumentTypeView, APIDocumentVersionsListView, APIDocumentVersionPageListView, APIDocumentVersionView, - APIRecentDocumentListView + APIRecentDocumentListView, + APIDocumentVersionPageView, + APIDocumentVersionPageImageView ) from .views.document_views import ( DocumentDocumentTypeEditView, DocumentDownloadFormView, DocumentDownloadView, DocumentDuplicatesListView, DocumentEditView, DocumentListView, DocumentPreviewView, DocumentPrint, - DocumentTransformationsClearView, DocumentTransformationsCloneView, - DocumentUpdatePageCountView, DocumentView, DuplicatedDocumentListView, - RecentAccessDocumentListView, RecentAddedDocumentListView + DocumentPagesResetView, DocumentTransformationsClearView, + DocumentTransformationsCloneView, DocumentView, + DuplicatedDocumentListView, RecentAccessDocumentListView, + RecentAddedDocumentListView ) from .views.document_page_views import ( DocumentPageDisable, DocumentPageEnable, DocumentPageListView, @@ -30,7 +33,8 @@ from .views.document_page_views import ( ) from .views.document_version_views import ( DocumentVersionDownloadFormView, DocumentVersionDownloadView, - DocumentVersionListView, DocumentVersionRevertView, DocumentVersionView, + DocumentVersionListView, DocumentVersionRevertView, + DocumentVersionUpdatePageCountView, DocumentVersionView, ) from .views.document_type_views import ( DocumentTypeCreateView, DocumentTypeDeleteView, @@ -172,14 +176,14 @@ urlpatterns_documents = [ name='document_print' ), url( - regex=r'^documents/(?P\d+)/reset_page_count/$', - view=DocumentUpdatePageCountView.as_view(), - name='document_update_page_count' + regex=r'^documents/(?P\d+)/pages/reset/$', + view=DocumentPagesResetView.as_view(), + name='document_pages_reset' ), url( - regex=r'^documents/multiple/reset_page_count/$', - view=DocumentUpdatePageCountView.as_view(), - name='document_multiple_update_page_count' + regex=r'^documents/multiple/pages/reset/$', + view=DocumentPagesResetView.as_view(), + name='document_multiple_pages_reset' ), url( regex=r'^documents/(?P\d+)/download/form/$', @@ -305,6 +309,16 @@ urlpatterns_document_versions = [ view=DocumentVersionDownloadView.as_view(), name='document_version_download' ), + url( + regex=r'^documents/versions/(?P\d+)/pages/update/$', + view=DocumentVersionUpdatePageCountView.as_view(), + name='document_version_page_count_update' + ), + url( + regex=r'^documents/versions/multiple/pages/update/$', + view=DocumentVersionUpdatePageCountView.as_view(), + name='document_version_multiple_page_count_update' + ), url( regex=r'^documents/versions/(?P\d+)/revert/$', view=DocumentVersionRevertView.as_view(), @@ -405,6 +419,11 @@ api_urls = [ view=APIDocumentVersionPageListView.as_view(), name='documentversion-page-list' ), + url( + regex=r'^documents/(?P[0-9]+)/pages/$', + view=APIDocumentPageListView.as_view(), + name='document-page-list' + ), url( regex=r'^documents/(?P[0-9]+)/versions/(?P[0-9]+)/download/$', view=APIDocumentVersionDownloadView.as_view(), @@ -416,12 +435,20 @@ api_urls = [ ), url( regex=r'^documents/(?P[0-9]+)/versions/(?P[0-9]+)/pages/(?P[0-9]+)$', + view=APIDocumentVersionPageView.as_view(), name='documentversionpage-detail' + ), + url( + regex=r'^documents/(?P[0-9]+)/pages/(?P[0-9]+)$', view=APIDocumentPageView.as_view(), name='documentpage-detail' ), url( - regex=r'^documents/(?P[0-9]+)/versions/(?P[0-9]+)/pages/(?P[0-9]+)/image/$', + regex=r'^documents/(?P[0-9]+)/pages/(?P[0-9]+)/image/$', view=APIDocumentPageImageView.as_view(), name='documentpage-image' ), + url( + regex=r'^documents/(?P[0-9]+)/versions/(?P[0-9]+)/pages/(?P[0-9]+)/image/$', + view=APIDocumentVersionPageImageView.as_view(), name='documentversionpage-image' + ), url( regex=r'^trashed_documents/$', view=APITrashedDocumentListView.as_view(), name='trasheddocument-list' diff --git a/mayan/apps/documents/views/document_page_views.py b/mayan/apps/documents/views/document_page_views.py index f05767d627..46fcacd9e7 100644 --- a/mayan/apps/documents/views/document_page_views.py +++ b/mayan/apps/documents/views/document_page_views.py @@ -20,8 +20,8 @@ from mayan.apps.converter.literals import DEFAULT_ROTATION, DEFAULT_ZOOM_LEVEL from ..forms import DocumentPageForm from ..icons import icon_document_pages -from ..links import link_document_update_page_count -from ..models import Document, DocumentPage +from ..links import link_document_pages_reset +from ..models import Document, DocumentPage, DocumentVersionPage from ..permissions import permission_document_edit, permission_document_view from ..settings import ( setting_rotation_step, setting_zoom_percent_step, setting_zoom_max_level, @@ -50,13 +50,13 @@ class DocumentPageListView(ExternalObjectMixin, SingleObjectListView): 'hide_object': True, 'list_as_items': True, 'no_results_icon': icon_document_pages, - 'no_results_main_link': link_document_update_page_count.resolve( + 'no_results_main_link': link_document_pages_reset.resolve( request=self.request, resolved_object=self.external_object ), 'no_results_text': _( 'This could mean that the document is of a format that is ' - 'not supported, that it is corrupted or that the upload ' - 'process was interrupted. Use the document page recalculation ' + 'not supported, that it is corrupted, or that the upload ' + 'process was interrupted. Use the document page reset ' 'action to attempt to introspect the page count again.' ), 'no_results_title': _('No document pages available'), diff --git a/mayan/apps/documents/views/document_version_views.py b/mayan/apps/documents/views/document_version_views.py index 4c53bbff36..8a2c3cd1ed 100644 --- a/mayan/apps/documents/views/document_version_views.py +++ b/mayan/apps/documents/views/document_version_views.py @@ -3,10 +3,11 @@ from __future__ import absolute_import, unicode_literals import logging from django.contrib import messages -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ungettext from mayan.apps.common.generics import ( - ConfirmView, SingleObjectDetailView, SingleObjectListView + ConfirmView, MultipleObjectConfirmActionView, SingleObjectDetailView, + SingleObjectListView ) from mayan.apps.common.mixins import ExternalObjectMixin @@ -14,9 +15,10 @@ from ..events import event_document_view from ..forms import DocumentVersionDownloadForm, DocumentVersionPreviewForm from ..models import Document, DocumentVersion from ..permissions import ( - permission_document_download, permission_document_version_revert, - permission_document_version_view + permission_document_download, permission_document_tools, + permission_document_version_revert, permission_document_version_view ) +from ..tasks import task_update_page_count from .document_views import DocumentDownloadFormView, DocumentDownloadView @@ -142,6 +144,45 @@ class DocumentVersionRevertView(ExternalObjectMixin, ConfirmView): ) +class DocumentVersionUpdatePageCountView(MultipleObjectConfirmActionView): + model = DocumentVersion + object_permission = permission_document_tools + success_message = _( + '%(count)d document version queued for page count recalculation' + ) + success_message_plural = _( + '%(count)d documents version queued for page count recalculation' + ) + + def get_extra_context(self): + queryset = self.object_list + + result = { + 'title': ungettext( + singular='Recalculate the page count of the selected document version?', + plural='Recalculate the page count of the selected document versions?', + number=queryset.count() + ) + } + + if queryset.count() == 1: + result.update( + { + 'object': queryset.first(), + 'title': _( + 'Recalculate the page count of the document version: %s?' + ) % queryset.first() + } + ) + + return result + + def object_action(self, form, instance): + task_update_page_count.apply_async( + kwargs={'version_id': instance.pk} + ) + + class DocumentVersionView(SingleObjectDetailView): form_class = DocumentVersionPreviewForm model = DocumentVersion diff --git a/mayan/apps/documents/views/document_views.py b/mayan/apps/documents/views/document_views.py index 566077c99c..dab008a75c 100644 --- a/mayan/apps/documents/views/document_views.py +++ b/mayan/apps/documents/views/document_views.py @@ -44,14 +44,14 @@ from ..permissions import ( from ..settings import ( setting_print_width, setting_print_height, setting_recent_added_count ) -from ..tasks import task_update_page_count +from ..tasks import task_document_pages_reset from ..utils import parse_range __all__ = ( 'DocumentListView', 'DocumentDocumentTypeEditView', 'DocumentDuplicatesListView', 'DocumentEditView', 'DocumentPreviewView', 'DocumentView', 'DocumentDownloadFormView', 'DocumentDownloadView', - 'DocumentUpdatePageCountView', 'DocumentTransformationsClearView', + 'DocumentPagesResetView', 'DocumentTransformationsClearView', 'DocumentTransformationsCloneView', 'DocumentPrint', 'DuplicatedDocumentListView', 'RecentAccessDocumentListView', 'RecentAddedDocumentListView' @@ -418,6 +418,52 @@ class DocumentPreviewView(SingleObjectDetailView): } +class DocumentPagesResetView(MultipleObjectConfirmActionView): + model = Document + object_permission = permission_document_tools + success_message = _('%(count)d document queued for pages reset') + success_message_plural = _('%(count)d documents queued for pages reset') + + def get_extra_context(self): + queryset = self.object_list + + result = { + 'title': ungettext( + singular='Reset the pages of the selected document?', + plural='Reset the pages of the selected documents?', + number=queryset.count() + ) + } + + if queryset.count() == 1: + result.update( + { + 'object': queryset.first(), + 'title': _( + 'Reset the pages of the document: %s?' + ) % queryset.first() + } + ) + + return result + + def object_action(self, form, instance): + latest_version = instance.latest_version + if latest_version: + task_document_pages_reset.apply_async( + kwargs={'document_id': instance.pk} + ) + else: + messages.error( + self.request, _( + 'Document "%(document)s" is empty. Upload at least one ' + 'document version before attempting to reset the pages. ' + ) % { + 'document': instance, + } + ) + + class DocumentView(SingleObjectDetailView): form_class = DocumentPropertiesForm model = Document @@ -436,57 +482,6 @@ class DocumentView(SingleObjectDetailView): } -class DocumentUpdatePageCountView(MultipleObjectConfirmActionView): - model = Document - object_permission = permission_document_tools - success_message = _( - '%(count)d document queued for page count recalculation' - ) - success_message_plural = _( - '%(count)d documents queued for page count recalculation' - ) - - def get_extra_context(self): - queryset = self.object_list - - result = { - 'title': ungettext( - singular='Recalculate the page count of the selected document?', - plural='Recalculate the page count of the selected documents?', - number=queryset.count() - ) - } - - if queryset.count() == 1: - result.update( - { - 'object': queryset.first(), - 'title': _( - 'Recalculate the page count of the document: %s?' - ) % queryset.first() - } - ) - - return result - - def object_action(self, form, instance): - latest_version = instance.latest_version - if latest_version: - task_update_page_count.apply_async( - kwargs={'version_id': latest_version.pk} - ) - else: - messages.error( - self.request, _( - 'Document "%(document)s" is empty. Upload at least one ' - 'document version before attempting to detect the ' - 'page count.' - ) % { - 'document': instance, - } - ) - - class DocumentTransformationsClearView(MultipleObjectConfirmActionView): model = Document object_permission = permission_transformation_delete diff --git a/mayan/apps/dynamic_search/classes.py b/mayan/apps/dynamic_search/classes.py index 2476f07a15..1630bcc7fb 100644 --- a/mayan/apps/dynamic_search/classes.py +++ b/mayan/apps/dynamic_search/classes.py @@ -184,7 +184,14 @@ class SearchModel(object): query_string=query_string, global_and_search=global_and_search ) - queryset = self.get_queryset().filter(search_query.query).distinct() + try: + queryset = self.get_queryset().filter(search_query.query).distinct() + except Exception: + logger.error( + 'Error filtering model %s with queryset: %s', self.model, + search_query.query + ) + raise if self.permission: queryset = AccessControlList.objects.restrict_queryset( diff --git a/mayan/apps/file_caching/migrations/0003_auto_20191008_1510.py b/mayan/apps/file_caching/migrations/0003_auto_20191008_1510.py new file mode 100644 index 0000000000..14382e0479 --- /dev/null +++ b/mayan/apps/file_caching/migrations/0003_auto_20191008_1510.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-10-08 15:10 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('file_caching', '0002_auto_20190729_0236'), + ] + + operations = [ + migrations.AlterField( + model_name='cache', + name='name', + field=models.CharField(db_index=True, help_text='Internal name of the cache.', max_length=128, unique=True, verbose_name='Name'), + ), + ] diff --git a/mayan/apps/file_metadata/apps.py b/mayan/apps/file_metadata/apps.py index a9830d467d..675ebe3365 100644 --- a/mayan/apps/file_metadata/apps.py +++ b/mayan/apps/file_metadata/apps.py @@ -12,7 +12,9 @@ from mayan.apps.common.menus import ( menu_tools ) from mayan.apps.document_indexing.handlers import handler_index_document -from mayan.apps.documents.search import document_page_search, document_search +from mayan.apps.documents.search import ( + document_page_search, document_search, document_version_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 @@ -156,11 +158,19 @@ class FileMetadataApp(MayanAppConfig): label=_('File metadata value') ) - document_page_search.add_model_field( + #document_page_search.add_model_field( + # field='document__document_version__file_metadata_drivers__entries__key', + # label=_('File metadata key') + #) + #document_page_search.add_model_field( + # field='document__document_version__file_metadata_drivers__entries__value', + # label=_('File metadata value') + #) + document_version_page_search.add_model_field( field='document_version__file_metadata_drivers__entries__key', label=_('File metadata key') ) - document_page_search.add_model_field( + document_version_page_search.add_model_field( field='document_version__file_metadata_drivers__entries__value', label=_('File metadata value') ) diff --git a/mayan/apps/metadata/apps.py b/mayan/apps/metadata/apps.py index 44fd6bb546..b0224e9ceb 100644 --- a/mayan/apps/metadata/apps.py +++ b/mayan/apps/metadata/apps.py @@ -16,7 +16,9 @@ from mayan.apps.common.menus import ( menu_facet, menu_list_facet, menu_multi_item, menu_object, menu_secondary, menu_setup ) -from mayan.apps.documents.search import document_page_search, document_search +from mayan.apps.documents.search import ( + document_page_search, document_search, document_version_page_search +) from mayan.apps.documents.signals import post_document_type_change from mayan.apps.events.classes import ModelEventType from mayan.apps.events.links import ( @@ -76,7 +78,7 @@ class MetadataApp(MayanAppConfig): app_label='documents', model_name='Document' ) DocumentPageResult = apps.get_model( - app_label='documents', model_name='DocumentPageResult' + app_label='documents', model_name='DocumentVersionPageResult' ) DocumentType = apps.get_model( @@ -188,10 +190,18 @@ class MetadataApp(MayanAppConfig): ) document_page_search.add_model_field( - field='document_version__document__metadata__metadata_type__name', + field='document__metadata__metadata_type__name', label=_('Metadata type') ) document_page_search.add_model_field( + field='document__metadata__value', + label=_('Metadata value') + ) + document_version_page_search.add_model_field( + field='document_version__document__metadata__metadata_type__name', + label=_('Metadata type') + ) + document_version_page_search.add_model_field( field='document_version__document__metadata__value', label=_('Metadata value') ) diff --git a/mayan/apps/ocr/admin.py b/mayan/apps/ocr/admin.py index 865481d602..9a927c5260 100644 --- a/mayan/apps/ocr/admin.py +++ b/mayan/apps/ocr/admin.py @@ -3,13 +3,15 @@ from __future__ import unicode_literals from django.contrib import admin from .models import ( - DocumentPageOCRContent, DocumentTypeSettings, DocumentVersionOCRError + DocumentTypeSettings, DocumentVersionPageOCRContent, + DocumentVersionOCRError ) -@admin.register(DocumentPageOCRContent) -class DocumentPageOCRContentAdmin(admin.ModelAdmin): - list_display = ('document_page',) +@admin.register(DocumentVersionPageOCRContent) +class DocumentVersionPageOCRContentAdmin(admin.ModelAdmin): + pass + #list_display = ('document_page',) @admin.register(DocumentTypeSettings) diff --git a/mayan/apps/ocr/api_views.py b/mayan/apps/ocr/api_views.py index 0dea74beb3..20a4fe833e 100644 --- a/mayan/apps/ocr/api_views.py +++ b/mayan/apps/ocr/api_views.py @@ -8,7 +8,7 @@ from rest_framework.response import Response from mayan.apps.documents.models import Document, DocumentVersion from mayan.apps.rest_api.permissions import MayanPermission -from .models import DocumentPageOCRContent +from .models import DocumentVersionPageOCRContent from .permissions import permission_ocr_content_view, permission_ocr_document from .serializers import DocumentPageOCRContentSerializer @@ -90,8 +90,8 @@ class APIDocumentPageOCRContentView(generics.RetrieveAPIView): try: ocr_content = instance.ocr_content - except DocumentPageOCRContent.DoesNotExist: - ocr_content = DocumentPageOCRContent.objects.none() + except DocumentVersionPageOCRContent.DoesNotExist: + ocr_content = DocumentVersionPageOCRContent.objects.none() serializer = self.get_serializer(ocr_content) return Response(serializer.data) diff --git a/mayan/apps/ocr/apps.py b/mayan/apps/ocr/apps.py index 33b4f1893b..772ec287fa 100644 --- a/mayan/apps/ocr/apps.py +++ b/mayan/apps/ocr/apps.py @@ -12,7 +12,9 @@ from mayan.apps.common.classes import ModelField from mayan.apps.common.menus import ( menu_facet, menu_list_facet, menu_multi_item, menu_secondary, menu_tools ) -from mayan.apps.documents.search import document_search, document_page_search +from mayan.apps.documents.search import ( + document_search, document_page_search, document_version_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 @@ -32,17 +34,19 @@ from .links import ( link_document_ocr_content_delete_multiple, link_document_ocr_download, link_document_ocr_errors_list, link_document_submit, link_document_submit_multiple, link_document_type_ocr_settings, - link_document_type_submit, link_entry_list + link_document_type_submit, link_document_version_page_ocr_content, + link_entry_list ) from .methods import ( - method_document_ocr_submit, method_document_version_ocr_submit + method_document_ocr_submit, method_document_page_get_ocr_content, + method_document_version_ocr_submit ) from .permissions import ( permission_document_type_ocr_setup, permission_ocr_document, permission_ocr_content_view ) from .signals import post_document_version_ocr -from .utils import get_document_ocr_content +from .utils import get_document_ocr_content, get_document_version_ocr_content logger = logging.getLogger(__name__) @@ -73,6 +77,9 @@ class OCRApp(MayanAppConfig): DocumentVersion = apps.get_model( app_label='documents', model_name='DocumentVersion' ) + DocumentVersionPage = apps.get_model( + app_label='documents', model_name='DocumentVersionPage' + ) DocumentVersionOCRError = self.get_model( model_name='DocumentVersionOCRError' @@ -81,8 +88,11 @@ class OCRApp(MayanAppConfig): Document.add_to_class( name='submit_for_ocr', value=method_document_ocr_submit ) + DocumentPage.add_to_class( + name='get_ocr_content', value=method_document_page_get_ocr_content + ) DocumentVersion.add_to_class( - name='ocr_content', value=get_document_ocr_content + name='ocr_content', value=get_document_version_ocr_content ) DocumentVersion.add_to_class( name='submit_for_ocr', value=method_document_version_ocr_submit @@ -97,7 +107,7 @@ class OCRApp(MayanAppConfig): ) ModelField( - model=Document, name='versions__version_pages__ocr_content__content' + model=Document, name='versions__pages__ocr_content__content' ) ModelPermission.register( @@ -128,12 +138,14 @@ class OCRApp(MayanAppConfig): ) document_search.add_model_field( - field='versions__version_pages__ocr_content__content', label=_('OCR') + field='versions__pages__ocr_content__content', label=_('OCR') ) - - document_page_search.add_model_field( + document_version_page_search.add_model_field( field='ocr_content__content', label=_('OCR') ) + #document_page_search.add_model_field( + # field='ocr_content__content', label=_('OCR') + #) menu_facet.bind_links( links=(link_document_ocr_content,), sources=(Document,) @@ -141,6 +153,10 @@ class OCRApp(MayanAppConfig): menu_list_facet.bind_links( links=(link_document_page_ocr_content,), sources=(DocumentPage,) ) + menu_list_facet.bind_links( + links=(link_document_version_page_ocr_content,), + sources=(DocumentVersionPage,) + ) menu_list_facet.bind_links( links=(link_document_type_ocr_settings,), sources=(DocumentType,) ) diff --git a/mayan/apps/ocr/forms.py b/mayan/apps/ocr/forms.py index 6951861ecf..5379bce4ae 100644 --- a/mayan/apps/ocr/forms.py +++ b/mayan/apps/ocr/forms.py @@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext from mayan.apps.common.widgets import TextAreaDiv -from .models import DocumentPageOCRContent +from .models import DocumentVersionPageOCRContent class DocumentPageOCRContentForm(forms.Form): @@ -28,15 +28,26 @@ class DocumentPageOCRContentForm(forms.Form): content = '' self.fields['contents'].initial = '' - try: - page_content = page.ocr_content.content - except DocumentPageOCRContent.DoesNotExist: - pass - else: - content = conditional_escape(force_text(page_content)) + content = conditional_escape( + force_text(self.get_instance_ocr_content(instance=page)) + ) self.fields['contents'].initial = mark_safe(content) + def get_instance_ocr_content(self, instance): + try: + return instance.content_object.ocr_content.content + except DocumentVersionPageOCRContent.DoesNotExist: + return '' + + +class DocumentVersionPageOCRContentForm(DocumentPageOCRContentForm): + def get_instance_ocr_content(self, instance): + try: + return instance.ocr_content.content + except (AttributeError, DocumentVersionPageOCRContent.DoesNotExist): + return '' + class DocumentOCRContentForm(forms.Form): """ @@ -54,19 +65,15 @@ class DocumentOCRContentForm(forms.Form): ) def __init__(self, *args, **kwargs): - self.document = kwargs.pop('instance', None) + document = kwargs.pop('instance', None) super(DocumentOCRContentForm, self).__init__(*args, **kwargs) content = [] self.fields['contents'].initial = '' - try: - document_pages = self.document.pages.all() - except AttributeError: - document_pages = [] - for page in document_pages: + for document_page in document.pages.all(): try: - page_content = page.ocr_content.content - except DocumentPageOCRContent.DoesNotExist: + page_content = document_page.content_object.ocr_content.content + except (AttributeError, DocumentVersionPageOCRContent.DoesNotExist): pass else: content.append(conditional_escape(force_text(page_content))) @@ -74,7 +81,7 @@ class DocumentOCRContentForm(forms.Form): '\n\n\n
- %s -

\n\n\n' % ( ugettext( 'Page %(page_number)d' - ) % {'page_number': page.page_number} + ) % {'page_number': document_page.page_number} ) ) diff --git a/mayan/apps/ocr/icons.py b/mayan/apps/ocr/icons.py index f6e1ce3141..f3f7984992 100644 --- a/mayan/apps/ocr/icons.py +++ b/mayan/apps/ocr/icons.py @@ -19,7 +19,7 @@ icon_document_ocr_errors_list = Icon( icon_document_type_ocr_settings = Icon( driver_name='fontawesome', symbol='font' ) -icon_document_type_submit = Icon(driver_name='fontawesome', symbol='font') -icon_entry_list = Icon(driver_name='fontawesome', symbol='font') - icon_document_submit = icon_document_multiple_submit +icon_document_type_submit = Icon(driver_name='fontawesome', symbol='font') +icon_document_version_page_ocr_content = Icon(driver_name='fontawesome', symbol='font') +icon_entry_list = Icon(driver_name='fontawesome', symbol='font') diff --git a/mayan/apps/ocr/links.py b/mayan/apps/ocr/links.py index 343bf26d18..68284d26ef 100644 --- a/mayan/apps/ocr/links.py +++ b/mayan/apps/ocr/links.py @@ -58,10 +58,11 @@ link_document_type_submit = Link( permissions=(permission_ocr_document,), text=_('OCR documents per type'), view='ocr:document_type_submit' ) -link_entry_list = Link( - icon_class_path='mayan.apps.ocr.icons.icon_entry_list', - permissions=(permission_ocr_document,), text=_('OCR errors'), - view='ocr:entry_list' +link_document_version_page_ocr_content = Link( + args='resolved_object.id', + icon_class_path='mayan.apps.ocr.icons.icon_document_version_page_ocr_content', + permissions=(permission_ocr_content_view,), text=_('OCR'), + view='ocr:document_version_page_ocr_content', ) link_document_ocr_errors_list = Link( args='resolved_object.id', @@ -75,3 +76,8 @@ link_document_ocr_download = Link( permissions=(permission_ocr_content_view,), text=_('Download OCR text'), view='ocr:document_ocr_download' ) +link_entry_list = Link( + icon_class_path='mayan.apps.ocr.icons.icon_entry_list', + permissions=(permission_ocr_document,), text=_('OCR errors'), + view='ocr:entry_list' +) diff --git a/mayan/apps/ocr/managers.py b/mayan/apps/ocr/managers.py index ee8e1bd95a..869c454001 100644 --- a/mayan/apps/ocr/managers.py +++ b/mayan/apps/ocr/managers.py @@ -9,7 +9,9 @@ from django.conf import settings from django.db import models, transaction from mayan.apps.documents.literals import DOCUMENT_IMAGE_TASK_TIMEOUT -from mayan.apps.documents.tasks import task_generate_document_page_image +from mayan.apps.documents.tasks import ( + task_generate_document_version_page_image +) from .events import ( event_ocr_document_content_deleted, event_ocr_document_version_finish @@ -20,47 +22,53 @@ from .signals import post_document_version_ocr logger = logging.getLogger(__name__) -class DocumentPageOCRContentManager(models.Manager): +class DocumentVesionPageOCRContentManager(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() + self.filter( + document_version_page=document_page.content_object + ).delete() event_ocr_document_content_deleted.commit( actor=user, target=document ) - def process_document_page(self, document_page): + def process_document_version_page(self, document_version_page): logger.info( 'Processing page: %d of document version: %s', - document_page.page_number, document_page.document_version + document_version_page.page_number, + document_version_page.document_version ) - DocumentPageOCRContent = apps.get_model( - app_label='ocr', model_name='DocumentPageOCRContent' + DocumentVersionPageOCRContent = apps.get_model( + app_label='ocr', model_name='DocumentVersionPageOCRContent' ) - task = task_generate_document_page_image.apply_async( + task = task_generate_document_version_page_image.apply_async( kwargs=dict( - document_page_id=document_page.pk + document_version_page_id=document_version_page.pk ) ) - cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT, disable_sync_subtasks=False) + cache_filename = task.get( + timeout=DOCUMENT_IMAGE_TASK_TIMEOUT, disable_sync_subtasks=False + ) - with document_page.cache_partition.get_file(filename=cache_filename).open() as file_object: - document_page_content, created = DocumentPageOCRContent.objects.get_or_create( - document_page=document_page + with document_version_page.cache_partition.get_file(filename=cache_filename).open() as file_object: + document_version_page_content, created = DocumentVersionPageOCRContent.objects.get_or_create( + document_version_page=document_version_page ) - document_page_content.content = ocr_backend.execute( + document_version_page_content.content = ocr_backend.execute( file_object=file_object, - language=document_page.document.language + language=document_version_page.document.language ) - document_page_content.save() + document_version_page_content.save() logger.info( 'Finished processing page: %d of document version: %s', - document_page.page_number, document_page.document_version + document_version_page.page_number, + document_version_page.document_version ) def process_document_version(self, document_version): @@ -68,8 +76,10 @@ class DocumentPageOCRContentManager(models.Manager): logger.debug('document version: %d', document_version.pk) try: - for document_page in document_version.pages.all(): - self.process_document_page(document_page=document_page) + for document_version_page in document_version.pages.all(): + self.process_document_version_page( + document_version_page=document_version_page + ) except Exception as exception: logger.error( 'OCR error for document version: %d; %s', document_version.pk, diff --git a/mayan/apps/ocr/methods.py b/mayan/apps/ocr/methods.py index b1c11a8265..567fa4b79a 100644 --- a/mayan/apps/ocr/methods.py +++ b/mayan/apps/ocr/methods.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from datetime import timedelta +from django.apps import apps from django.utils.timezone import now from mayan.apps.common.settings import settings_db_sync_task_delay @@ -17,6 +18,17 @@ def method_document_ocr_submit(self): latest_version.submit_for_ocr() +def method_document_page_get_ocr_content(self): + DocumentVersionPageOCRContent = apps.get_model( + app_label='ocr', model_name='DocumentVersionPageOCRContent' + ) + + try: + return self.content_object.ocr_content.content + except (AttributeError, DocumentVersionPageOCRContent.DoesNotExist): + return None + + def method_document_version_ocr_submit(self): event_ocr_document_version_submit.commit( action_object=self.document, target=self diff --git a/mayan/apps/ocr/migrations/0009_rename_page_content.py b/mayan/apps/ocr/migrations/0009_rename_page_content.py new file mode 100644 index 0000000000..f98ee91251 --- /dev/null +++ b/mayan/apps/ocr/migrations/0009_rename_page_content.py @@ -0,0 +1,42 @@ +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ('ocr', '0008_auto_20180917_0646'), + ('documents', '0052_rename_document_page'), + ] + + operations = [ + migrations.RenameModel( + 'DocumentPageOCRContent', 'DocumentVersionPageOCRContent' + ), + migrations.AlterField( + model_name='documentversionpageocrcontent', + name='document_page', + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + #name='document_version_page', + related_name='ocr_content', + to='documents.DocumentVersionPage', + verbose_name='Document version page' + ), + ), + migrations.RenameField( + model_name='documentversionpageocrcontent', + old_name='document_page', + new_name='document_version_page', + ), + migrations.AlterModelOptions( + name='documentversionpageocrcontent', + options={ + 'verbose_name': 'Document version page OCR content', + 'verbose_name_plural': 'Document version pages OCR contents' + }, + ), + ] + + diff --git a/mayan/apps/ocr/models.py b/mayan/apps/ocr/models.py index 9f42843a7e..adaf142a0c 100644 --- a/mayan/apps/ocr/models.py +++ b/mayan/apps/ocr/models.py @@ -4,10 +4,12 @@ from django.db import models from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from mayan.apps.documents.models import DocumentPage, DocumentType, DocumentVersion +from mayan.apps.documents.models import ( + DocumentPage, DocumentType, DocumentVersion, DocumentVersionPage +) from .managers import ( - DocumentPageOCRContentManager, DocumentTypeSettingsManager + DocumentVesionPageOCRContentManager, DocumentTypeSettingsManager ) @@ -36,13 +38,13 @@ class DocumentTypeSettings(models.Model): @python_2_unicode_compatible -class DocumentPageOCRContent(models.Model): +class DocumentVersionPageOCRContent(models.Model): """ This model stores the OCR results for a document page. """ - document_page = models.OneToOneField( + document_version_page = models.OneToOneField( on_delete=models.CASCADE, related_name='ocr_content', - to=DocumentPage, verbose_name=_('Document page') + to=DocumentVersionPage, verbose_name=_('Document version page') ) content = models.TextField( blank=True, help_text=_( @@ -50,11 +52,11 @@ class DocumentPageOCRContent(models.Model): ), verbose_name=_('Content') ) - objects = DocumentPageOCRContentManager() + objects = DocumentVesionPageOCRContentManager() class Meta: - verbose_name = _('Document page OCR content') - verbose_name_plural = _('Document pages OCR contents') + verbose_name = _('Document version page OCR content') + verbose_name_plural = _('Document version pages OCR contents') def __str__(self): return force_text(self.document_page) diff --git a/mayan/apps/ocr/serializers.py b/mayan/apps/ocr/serializers.py index 3d9c06c18d..780e9a48a6 100644 --- a/mayan/apps/ocr/serializers.py +++ b/mayan/apps/ocr/serializers.py @@ -2,10 +2,10 @@ from __future__ import unicode_literals from rest_framework import serializers -from .models import DocumentPageOCRContent +from .models import DocumentVersionPageOCRContent class DocumentPageOCRContentSerializer(serializers.ModelSerializer): class Meta: fields = ('content',) - model = DocumentPageOCRContent + model = DocumentVersionPageOCRContent diff --git a/mayan/apps/ocr/tasks.py b/mayan/apps/ocr/tasks.py index a6d9c82efb..4ba1f5268f 100644 --- a/mayan/apps/ocr/tasks.py +++ b/mayan/apps/ocr/tasks.py @@ -19,8 +19,8 @@ def task_do_ocr(self, document_version_pk): DocumentVersion = apps.get_model( app_label='documents', model_name='DocumentVersion' ) - DocumentPageOCRContent = apps.get_model( - app_label='ocr', model_name='DocumentPageOCRContent' + DocumentVersionPageOCRContent = apps.get_model( + app_label='ocr', model_name='DocumentVersionPageOCRContent' ) lock_id = 'task_do_ocr_doc_version-%d' % document_version_pk @@ -39,7 +39,7 @@ def task_do_ocr(self, document_version_pk): 'Starting document OCR for document version: %s', document_version ) - DocumentPageOCRContent.objects.process_document_version( + DocumentVersionPageOCRContent.objects.process_document_version( document_version=document_version ) except OperationalError as exception: diff --git a/mayan/apps/ocr/tests/test_api.py b/mayan/apps/ocr/tests/test_api.py index e847550141..578e8a94b8 100644 --- a/mayan/apps/ocr/tests/test_api.py +++ b/mayan/apps/ocr/tests/test_api.py @@ -12,13 +12,34 @@ from ..permissions import ( from .literals import TEST_DOCUMENT_CONTENT -class OCRAPITestCase(DocumentTestMixin, BaseAPITestCase): +class OCRAPIViewTestMixin(object): def _request_document_ocr_submit_view(self): return self.post( viewname='rest_api:document-ocr-submit-view', kwargs={'pk': self.test_document.pk} ) + def _request_document_version_ocr_submit_view(self): + return self.post( + viewname='rest_api:document-version-ocr-submit-view', kwargs={ + 'document_pk': self.test_document.pk, + 'version_pk': self.test_document.latest_version.pk + } + ) + + def _request_document_version_page_content_view(self): + return self.get( + viewname='rest_api:document-page-ocr-content-view', kwargs={ + 'document_pk': self.test_document.pk, + 'version_pk': self.test_document.latest_version.pk, + 'page_pk': self.test_document.latest_version.pages.first().pk, + } + ) + + +class OCRAPIViewTestCase( + OCRAPIViewTestMixin, DocumentTestMixin, BaseAPITestCase +): def test_submit_document_no_access(self): response = self._request_document_ocr_submit_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -35,15 +56,9 @@ class OCRAPITestCase(DocumentTestMixin, BaseAPITestCase): self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) self.assertTrue( - hasattr(self.test_document.pages.first(), 'ocr_content') - ) - - def _request_document_version_ocr_submit_view(self): - return self.post( - viewname='rest_api:document-version-ocr-submit-view', kwargs={ - 'document_pk': self.test_document.pk, - 'version_pk': self.test_document.latest_version.pk - } + hasattr( + self.test_document.pages.first().content_object, 'ocr_content' + ) ) def test_submit_document_version_no_access(self): @@ -62,20 +77,11 @@ class OCRAPITestCase(DocumentTestMixin, BaseAPITestCase): self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) self.assertTrue( - hasattr(self.test_document.pages.first(), 'ocr_content') - ) - - def _request_document_page_content_view(self): - return self.get( - viewname='rest_api:document-page-ocr-content-view', kwargs={ - 'document_pk': self.test_document.pk, - 'version_pk': self.test_document.latest_version.pk, - 'page_pk': self.test_document.latest_version.pages.first().pk, - } + hasattr(self.test_document_version.pages.first(), 'ocr_content') ) def test_get_document_version_page_content_no_access(self): - response = self._request_document_page_content_view() + response = self._request_document_version_page_content_view() self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_get_document_version_page_content_with_access(self): @@ -83,7 +89,7 @@ class OCRAPITestCase(DocumentTestMixin, BaseAPITestCase): self.grant_access( permission=permission_ocr_content_view, obj=self.test_document ) - response = self._request_document_page_content_view() + response = self._request_document_version_page_content_view() self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue( TEST_DOCUMENT_CONTENT in response.data['content'] diff --git a/mayan/apps/ocr/tests/test_events.py b/mayan/apps/ocr/tests/test_events.py index 4a4318ec64..c51cee8fad 100644 --- a/mayan/apps/ocr/tests/test_events.py +++ b/mayan/apps/ocr/tests/test_events.py @@ -8,13 +8,13 @@ from ..events import ( event_ocr_document_content_deleted, event_ocr_document_version_submit, event_ocr_document_version_finish ) -from ..models import DocumentPageOCRContent +from ..models import DocumentVersionPageOCRContent class OCREventsTestCase(GenericDocumentTestCase): def test_document_content_deleted_event(self): Action.objects.all().delete() - DocumentPageOCRContent.objects.delete_content_for( + DocumentVersionPageOCRContent.objects.delete_content_for( document=self.test_document ) diff --git a/mayan/apps/ocr/tests/test_models.py b/mayan/apps/ocr/tests/test_models.py index cd4992d0d4..fe31995990 100644 --- a/mayan/apps/ocr/tests/test_models.py +++ b/mayan/apps/ocr/tests/test_models.py @@ -19,7 +19,7 @@ class DocumentOCRTestCase(DocumentTestMixin, BaseTestCase): _skip_file_descriptor_test = True def test_ocr_language_backends_end(self): - content = self.test_document.pages.first().ocr_content.content + content = self.test_document.pages.first().content_object.ocr_content.content self.assertTrue(TEST_DOCUMENT_CONTENT in content) @@ -40,7 +40,7 @@ class GermanOCRSupportTestCase(DocumentTestMixin, BaseTestCase): ) def test_ocr_language_backends_end(self): - content = self.test_document.pages.first().ocr_content.content + content = self.test_document.pages.first().content_object.ocr_content.content self.assertTrue( TEST_DOCUMENT_CONTENT_DEU_1 in content diff --git a/mayan/apps/ocr/tests/test_views.py b/mayan/apps/ocr/tests/test_views.py index 6ed278c54a..03a7ec7baf 100644 --- a/mayan/apps/ocr/tests/test_views.py +++ b/mayan/apps/ocr/tests/test_views.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from mayan.apps.documents.tests.base import GenericDocumentViewTestCase -from ..models import DocumentPageOCRContent +from ..models import DocumentVersionPageOCRContent from ..permissions import ( permission_ocr_content_view, permission_ocr_document, permission_document_type_ocr_setup @@ -27,10 +27,10 @@ class OCRViewTestMixin(object): } ) - def _request_document_page_content_view(self): + def _request_document_version_page_content_view(self): return self.get( - viewname='ocr:document_page_ocr_content', kwargs={ - 'pk': self.test_document.pages.first().pk + viewname='ocr:document_version_page_ocr_content', kwargs={ + 'pk': self.test_document_version.pages.first().pk } ) @@ -86,8 +86,8 @@ class OCRViewsTestCase(OCRViewTestMixin, GenericDocumentViewTestCase): self.assertEqual(response.status_code, 404) self.assertTrue( - DocumentPageOCRContent.objects.filter( - document_page=self.test_document.pages.first() + DocumentVersionPageOCRContent.objects.filter( + document_version_page=self.test_document.pages.first().content_object ).exists() ) @@ -101,28 +101,11 @@ class OCRViewsTestCase(OCRViewTestMixin, GenericDocumentViewTestCase): self.assertEqual(response.status_code, 302) self.assertFalse( - DocumentPageOCRContent.objects.filter( - document_page=self.test_document.pages.first() + DocumentVersionPageOCRContent.objects.filter( + document_version_page=self.test_document.pages.first().content_object ).exists() ) - def test_document_page_content_view_no_permissions(self): - self.test_document.submit_for_ocr() - - response = self._request_document_page_content_view() - self.assertEqual(response.status_code, 404) - - def test_document_page_content_view_with_access(self): - self.test_document.submit_for_ocr() - self.grant_access( - obj=self.test_document, permission=permission_ocr_content_view - ) - - response = self._request_document_page_content_view() - self.assertContains( - response=response, text=TEST_DOCUMENT_CONTENT, status_code=200 - ) - def test_document_submit_view_no_permission(self): response = self._request_document_submit_view() self.assertEqual(response.status_code, 404) @@ -188,6 +171,23 @@ class OCRViewsTestCase(OCRViewTestMixin, GenericDocumentViewTestCase): ), ) + def test_document_version_page_content_view_no_permissions(self): + self.test_document.submit_for_ocr() + + response = self._request_document_version_page_content_view() + self.assertEqual(response.status_code, 404) + + def test_document_version_page_content_view_with_access(self): + self.test_document.submit_for_ocr() + self.grant_access( + obj=self.test_document, permission=permission_ocr_content_view + ) + + response = self._request_document_version_page_content_view() + self.assertContains( + response=response, text=TEST_DOCUMENT_CONTENT, status_code=200 + ) + class DocumentTypeOCRViewTestMixin(object): def _request_document_type_ocr_settings_view(self): diff --git a/mayan/apps/ocr/urls.py b/mayan/apps/ocr/urls.py index 3d2d9b0b25..a8b9d9791e 100644 --- a/mayan/apps/ocr/urls.py +++ b/mayan/apps/ocr/urls.py @@ -8,9 +8,10 @@ from .api_views import ( ) from .views import ( DocumentOCRContentDeleteView, DocumentOCRContentView, - DocumentOCRDownloadView, - DocumentOCRErrorsListView, DocumentPageOCRContentView, DocumentSubmitView, - DocumentTypeSettingsEditView, DocumentTypeSubmitView, EntryListView + DocumentOCRDownloadView, DocumentOCRErrorsListView, + DocumentPageOCRContentView, DocumentSubmitView, + DocumentTypeSettingsEditView, DocumentTypeSubmitView, + DocumentVersionPageOCRContentView, EntryListView ) urlpatterns = [ @@ -50,6 +51,11 @@ urlpatterns = [ view=DocumentPageOCRContentView.as_view(), name='document_page_ocr_content' ), + url( + regex=r'^documents/versions/pages/(?P\d+)/content/$', + view=DocumentVersionPageOCRContentView.as_view(), + name='document_version_page_ocr_content' + ), url( regex=r'^document_types/submit/$', view=DocumentTypeSubmitView.as_view(), name='document_type_submit' diff --git a/mayan/apps/ocr/utils.py b/mayan/apps/ocr/utils.py index 0ac5de5fb3..a192613999 100644 --- a/mayan/apps/ocr/utils.py +++ b/mayan/apps/ocr/utils.py @@ -4,15 +4,29 @@ from django.apps import apps from django.utils.encoding import force_text -def get_document_ocr_content(document): - DocumentPageOCRContent = apps.get_model( - app_label='ocr', model_name='DocumentPageOCRContent' +def get_document_version_ocr_content(document_version): + DocumentVersionPageOCRContent = apps.get_model( + app_label='ocr', model_name='DocumentVersionPageOCRContent' ) - for page in document.pages.all(): + for document_version_page in document_version.pages.all(): try: - page_content = page.ocr_content.content - except DocumentPageOCRContent.DoesNotExist: + page_content = document_version_page.ocr_content.content + except DocumentVersionPageOCRContent.DoesNotExist: + pass + else: + yield force_text(page_content) + + +def get_document_ocr_content(document): + DocumentVersionPageOCRContent = apps.get_model( + app_label='ocr', model_name='DocumentVersionPageOCRContent' + ) + + for document_page in document.pages.all(): + try: + page_content = document_page.content_object.ocr_content.content + except (AttributeError, DocumentVersionPageOCRContent.DoesNotExist): pass else: yield force_text(page_content) diff --git a/mayan/apps/ocr/views.py b/mayan/apps/ocr/views.py index 41b978388e..21e7c00fb0 100644 --- a/mayan/apps/ocr/views.py +++ b/mayan/apps/ocr/views.py @@ -12,10 +12,15 @@ from mayan.apps.common.generics import ( ) from mayan.apps.common.mixins import ExternalObjectMixin from mayan.apps.documents.forms import DocumentTypeFilteredSelectForm -from mayan.apps.documents.models import Document, DocumentPage, DocumentType +from mayan.apps.documents.models import ( + Document, DocumentPage, DocumentType, DocumentVersionPage +) -from .forms import DocumentPageOCRContentForm, DocumentOCRContentForm -from .models import DocumentPageOCRContent, DocumentVersionOCRError +from .forms import ( + DocumentPageOCRContentForm, DocumentOCRContentForm, + DocumentVersionPageOCRContentForm +) +from .models import DocumentVersionPageOCRContent, DocumentVersionOCRError from .permissions import ( permission_ocr_content_view, permission_ocr_document, permission_document_type_ocr_setup @@ -46,7 +51,7 @@ class DocumentOCRContentDeleteView(MultipleObjectConfirmActionView): return result def object_action(self, form, instance): - DocumentPageOCRContent.objects.delete_content_for( + DocumentVersionPageOCRContent.objects.delete_content_for( document=instance, user=self.request.user ) @@ -94,6 +99,30 @@ class DocumentPageOCRContentView(SingleObjectDetailView): } +class DocumentVersionPageOCRContentView(SingleObjectDetailView): + form_class = DocumentVersionPageOCRContentForm + model = DocumentVersionPage + object_permission = permission_ocr_content_view + + def dispatch(self, request, *args, **kwargs): + result = super(DocumentVersionPageOCRContentView, self).dispatch( + request, *args, **kwargs + ) + self.get_object().document.add_as_recent_document_for_user( + user=request.user + ) + return result + + def get_extra_context(self): + return { + 'hide_labels': True, + 'object': self.get_object(), + 'title': _( + 'OCR result for document version page: %s' + ) % self.get_object(), + } + + class DocumentSubmitView(MultipleObjectConfirmActionView): model = Document object_permission = permission_ocr_document diff --git a/mayan/apps/sources/apps.py b/mayan/apps/sources/apps.py index 7139e5a846..d948a54fa7 100644 --- a/mayan/apps/sources/apps.py +++ b/mayan/apps/sources/apps.py @@ -21,9 +21,10 @@ from .handlers import ( handler_create_default_document_source, handler_initialize_periodic_tasks ) from .links import ( - link_document_create_multiple, link_setup_sources, - link_setup_source_check_now, link_setup_source_create_imap_email, - link_setup_source_create_pop3_email, link_setup_source_create_sane_scanner, + link_document_create_multiple, link_document_pages_append, + link_setup_sources, link_setup_source_check_now, + link_setup_source_create_imap_email, link_setup_source_create_pop3_email, + link_setup_source_create_sane_scanner, link_setup_source_create_watch_folder, link_setup_source_create_webform, link_setup_source_create_staging_folder, link_setup_source_delete, link_setup_source_edit, link_setup_source_logs, link_staging_file_delete, @@ -145,10 +146,16 @@ class SourcesApp(MayanAppConfig): menu_secondary.bind_links( links=(link_document_version_upload,), sources=( - 'documents:document_version_list', 'documents:upload_version', + 'documents:document_version_list', 'sources:upload_version', 'documents:document_version_revert' ) ) + menu_secondary.bind_links( + links=(link_document_pages_append,), + sources=( + 'documents:document_pages', 'sources:document_pages_append' + ) + ) post_upgrade.connect( receiver=handler_initialize_periodic_tasks, diff --git a/mayan/apps/sources/forms.py b/mayan/apps/sources/forms.py index a6431f71e5..e4eed9de64 100644 --- a/mayan/apps/sources/forms.py +++ b/mayan/apps/sources/forms.py @@ -23,14 +23,25 @@ class NewDocumentForm(DocumentForm): class NewVersionForm(forms.Form): - def __init__(self, *args, **kwargs): - super(NewVersionForm, self).__init__(*args, **kwargs) + comment = forms.CharField( + help_text=_('An optional comment to explain the upload.'), + label=_('Comment'), required=False, + widget=forms.widgets.Textarea(attrs={'rows': 4}), + ) - self.fields['comment'] = forms.CharField( - label=_('Comment'), - required=False, - widget=forms.widgets.Textarea(attrs={'rows': 4}), - ) + append_pages = forms.BooleanField( + help_text=_( + 'If selected, the pages of the file uploaded will be appended ' + 'to the existing document pages. Otherwise the pages of the ' + 'upload will replace the existing pages of the document.' + ), label=_('Append pages?'), required=False, + ) + + def __init__(self, *args, **kwargs): + hide_append_pages = kwargs.pop('hide_append_pages', False) + super(NewVersionForm, self).__init__(*args, **kwargs) + if hide_append_pages: + self.fields['append_pages'].widget = forms.widgets.HiddenInput() class UploadBaseForm(forms.Form): diff --git a/mayan/apps/sources/icons.py b/mayan/apps/sources/icons.py index 1b0a6c8c84..a0a59fcded 100644 --- a/mayan/apps/sources/icons.py +++ b/mayan/apps/sources/icons.py @@ -9,6 +9,10 @@ icon_document_version_upload = Icon( driver_name='fontawesome', symbol='upload' ) icon_log = Icon(driver_name='fontawesome', symbol='exclamation-triangle') +icon_document_pages_append = Icon( + driver_name='fontawesome-dual', primary_symbol='copy', + secondary_symbol='plus' +) icon_setup_sources = Icon(driver_name='fontawesome', symbol='upload') icon_setup_source_check_now = Icon(driver_name='fontawesome', symbol='check') icon_setup_source_delete = Icon(driver_name='fontawesome', symbol='times') diff --git a/mayan/apps/sources/links.py b/mayan/apps/sources/links.py index b1793547f5..da5d3300d6 100644 --- a/mayan/apps/sources/links.py +++ b/mayan/apps/sources/links.py @@ -113,6 +113,13 @@ link_staging_file_delete = Link( permissions=(permission_document_new_version, permission_document_create), tags='dangerous', text=_('Delete'), view='sources:staging_file_delete', ) +link_document_pages_append = Link( + args='resolved_object.pk', + icon_class_path='mayan.apps.sources.icons.icon_document_pages_append', + permissions=(permission_document_new_version,), + text=_('Append pages'), + view='sources:document_pages_append' +) link_document_version_upload = Link( args='resolved_object.pk', condition=document_new_version_not_blocked, icon_class_path='mayan.apps.sources.icons.icon_document_version_upload', diff --git a/mayan/apps/sources/urls.py b/mayan/apps/sources/urls.py index fdc7adc330..173891f5f5 100644 --- a/mayan/apps/sources/urls.py +++ b/mayan/apps/sources/urls.py @@ -7,9 +7,10 @@ from .api_views import ( APIStagingSourceListView, APIStagingSourceView ) from .views import ( - SetupSourceCheckView, SetupSourceCreateView, SetupSourceDeleteView, - SetupSourceEditView, SetupSourceListView, SourceLogListView, - StagingFileDeleteView, UploadInteractiveVersionView, UploadInteractiveView + DocumentPagesAppendView, SetupSourceCheckView, SetupSourceCreateView, + SetupSourceDeleteView, SetupSourceEditView, SetupSourceListView, + SourceLogListView, StagingFileDeleteView, UploadInteractiveVersionView, + UploadInteractiveView ) from .wizards import DocumentCreateWizard @@ -41,6 +42,14 @@ urlpatterns = [ regex=r'^documents/(?P\d+)/versions/upload/interactive/$', view=UploadInteractiveVersionView.as_view(), name='upload_version' ), + url( + regex=r'^documents/(?P\d+)/pages/append/interactive/(?P\d+)/$', + view=DocumentPagesAppendView.as_view(), name='document_pages_append' + ), + url( + regex=r'^documents/(?P\d+)/pages/append/interactive/$', + view=DocumentPagesAppendView.as_view(), name='document_pages_append' + ), # Setup views diff --git a/mayan/apps/sources/views.py b/mayan/apps/sources/views.py index be55047b7d..3f26a650c2 100644 --- a/mayan/apps/sources/views.py +++ b/mayan/apps/sources/views.py @@ -362,7 +362,6 @@ class UploadInteractiveView(UploadBaseView): class UploadInteractiveVersionView(UploadBaseView): def dispatch(self, request, *args, **kwargs): - self.subtemplates_list = [] self.document = get_object_or_404( @@ -417,12 +416,17 @@ class UploadInteractiveVersionView(UploadBaseView): else: user_id = None - task_upload_new_version.apply_async(kwargs=dict( - shared_uploaded_file_id=shared_uploaded_file.pk, - document_id=self.document.pk, - user_id=user_id, - comment=forms['document_form'].cleaned_data.get('comment') - )) + task_upload_new_version.apply_async( + kwargs=dict( + append_pages=forms['document_form'].cleaned_data.get( + 'append_pages', False + ), + shared_uploaded_file_id=shared_uploaded_file.pk, + document_id=self.document.pk, + user_id=user_id, + comment=forms['document_form'].cleaned_data.get('comment') + ) + ) messages.success( message=_( @@ -448,13 +452,6 @@ class UploadInteractiveVersionView(UploadBaseView): files=kwargs.get('files', None), ) - def create_document_form_form(self, **kwargs): - return self.get_form_classes()['document_form']( - prefix=kwargs['prefix'], - data=kwargs.get('data', None), - files=kwargs.get('files', None), - ) - def get_form_classes(self): return { 'document_form': NewVersionForm, @@ -467,8 +464,33 @@ class UploadInteractiveVersionView(UploadBaseView): ).get_context_data(**kwargs) context['object'] = self.document context['title'] = _( - 'Upload a new version from source: %s' - ) % self.source.label + 'Upload a new version for document "%(document)s" from source: %(source)s' + ) % {'document': self.document, 'source': self.source.label} + + context['submit_label'] = _('Submit') + + return context + + +class DocumentPagesAppendView(UploadInteractiveVersionView): + def get_document_form_initial(self): + return { + 'append_pages': True, + } + + def get_form_extra_kwargs(self, form_name): + if form_name == 'document_form': + return { + 'hide_append_pages': True + } + + def get_context_data(self, **kwargs): + context = super( + DocumentPagesAppendView, self + ).get_context_data(**kwargs) + context['title'] = _( + 'Append pages to document "%(document)s" from source: %(source)s' + ) % {'document': self.document, 'source': self.source.label} return context diff --git a/mayan/apps/tags/apps.py b/mayan/apps/tags/apps.py index c64811558a..2828e06049 100644 --- a/mayan/apps/tags/apps.py +++ b/mayan/apps/tags/apps.py @@ -13,7 +13,9 @@ from mayan.apps.common.menus import ( menu_facet, menu_list_facet, menu_main, menu_multi_item, menu_object, menu_secondary ) -from mayan.apps.documents.search import document_page_search, document_search +from mayan.apps.documents.search import ( + document_page_search, document_search, document_version_page_search +) from mayan.apps.events.classes import ModelEventType from mayan.apps.events.links import ( link_events_for_object, link_object_event_types_user_subcriptions_list, @@ -62,7 +64,7 @@ class TagsApp(MayanAppConfig): ) DocumentPageResult = apps.get_model( - app_label='documents', model_name='DocumentPageResult' + app_label='documents', model_name='DocumentVersionPageResult' ) DocumentTag = self.get_model(model_name='DocumentTag') @@ -133,9 +135,12 @@ class TagsApp(MayanAppConfig): ) document_page_search.add_model_field( - field='document_version__document__tags__label', label=_('Tags') + field='document__tags__label', label=_('Tags') ) document_search.add_model_field(field='tags__label', label=_('Tags')) + document_version_page_search.add_model_field( + field='document_version__document__tags__label', label=_('Tags') + ) menu_facet.bind_links( links=(link_document_tag_list,), sources=(Document,) diff --git a/requirements/base.txt b/requirements/base.txt index 337001f564..87dd80811c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,9 +1,7 @@ Pillow==6.0.0 PyPDF2==1.26.0 PyYAML==5.1.1 -celery==4.3.0 django-activity-stream==0.7.0 -django-celery-beat==1.5.0 django-colorful==1.3 django-cors-headers==2.5.2 django-downloadview==1.9 diff --git a/setup.py b/setup.py index 5b4c273ef0..a8ab7ec567 100644 --- a/setup.py +++ b/setup.py @@ -60,9 +60,7 @@ django==1.11.24 Pillow==6.0.0 PyPDF2==1.26.0 PyYAML==5.1.1 -celery==4.3.0 django-activity-stream==0.7.0 -django-celery-beat==1.5.0 django-colorful==1.3 django-cors-headers==2.5.2 django-downloadview==1.9