Add support for new document page structure

Documents now have their own dedicated DocumentPage
submodel. The old DocumentPage is now called DocumentVersionPage.
This allows mappings between document pages and document version
pages, allowing renumbering, appending pages.
DocumentPages have a content_object to map them to any other
object. For now they only map to DocumentVersionPages.
New option added to the version upload form to append the
pages of the new version.
A new view was added to just append new pages with wraps the
new document version upload form and hides the append pages
checkbox set to True.
Add a new action, reset_pages to reset the pages of the
document to those of the latest version.

Missing: appending tests, checks for proper content_object in OCR and
document parsing.

Author: Roberto Rosario <roberto.rosario@mayan-edms.com>
Date:   Thu Oct 11 12:00:25 2019 -0400
This commit is contained in:
Roberto Rosario
2019-10-10 11:55:42 -04:00
parent 4a99a9df3e
commit 0699ad0556
87 changed files with 1973 additions and 707 deletions

View File

@@ -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

View File

@@ -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'

View File

@@ -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,)

View File

@@ -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:

View File

@@ -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,)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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<hr/><div class="document-page-content-divider">- %s -</div><hr/>\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))

View File

@@ -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'
)

View File

@@ -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'),

View File

@@ -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

View File

@@ -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'
},
),
]

View File

@@ -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):
"""

View File

@@ -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):

View File

@@ -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

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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()
)

View File

@@ -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<pk>\d+)/content/$',
view=DocumentPageContentView.as_view(), name='document_page_content'
),
url(
regex=r'^documents/versions/pages/(?P<pk>\d+)/content/$',
view=DocumentVersionPageContentView.as_view(),
name='document_version_page_content'
),
url(
regex=r'^documents/(?P<pk>\d+)/submit/$',
view=DocumentSubmitView.as_view(), name='document_submit'

View File

@@ -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))

View File

@@ -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

View File

@@ -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'

View File

@@ -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'])

View File

@@ -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,)
)

View File

@@ -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)

View File

@@ -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')

View File

@@ -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',

View File

@@ -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

View File

@@ -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()

View File

@@ -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'
},
),
]

View File

@@ -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',),
),
]

View File

@@ -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
),
]

View File

@@ -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

View File

@@ -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()

View File

@@ -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')

View File

@@ -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():

View File

@@ -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')

View File

@@ -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')

View File

@@ -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')
)

View File

@@ -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()

View File

@@ -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(

View File

@@ -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(

View File

@@ -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')

View File

@@ -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
}
)

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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<pk>\d+)/reset_page_count/$',
view=DocumentUpdatePageCountView.as_view(),
name='document_update_page_count'
regex=r'^documents/(?P<pk>\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<pk>\d+)/download/form/$',
@@ -305,6 +309,16 @@ urlpatterns_document_versions = [
view=DocumentVersionDownloadView.as_view(),
name='document_version_download'
),
url(
regex=r'^documents/versions/(?P<pk>\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<pk>\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<pk>[0-9]+)/pages/$',
view=APIDocumentPageListView.as_view(),
name='document-page-list'
),
url(
regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/download/$',
view=APIDocumentVersionDownloadView.as_view(),
@@ -416,12 +435,20 @@ api_urls = [
),
url(
regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)$',
view=APIDocumentVersionPageView.as_view(), name='documentversionpage-detail'
),
url(
regex=r'^documents/(?P<pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)$',
view=APIDocumentPageView.as_view(), name='documentpage-detail'
),
url(
regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)/image/$',
regex=r'^documents/(?P<pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)/image/$',
view=APIDocumentPageImageView.as_view(), name='documentpage-image'
),
url(
regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)/image/$',
view=APIDocumentVersionPageImageView.as_view(), name='documentversionpage-image'
),
url(
regex=r'^trashed_documents/$',
view=APITrashedDocumentListView.as_view(), name='trasheddocument-list'

View File

@@ -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'),

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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'),
),
]

View File

@@ -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')
)

View File

@@ -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')
)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,)
)

View File

@@ -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<hr/><div class="document-page-content-divider">- %s -</div><hr/>\n\n\n' % (
ugettext(
'Page %(page_number)d'
) % {'page_number': page.page_number}
) % {'page_number': document_page.page_number}
)
)

View File

@@ -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')

View File

@@ -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'
)

View File

@@ -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,

View File

@@ -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

View File

@@ -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'
},
),
]

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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']

View File

@@ -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
)

View File

@@ -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

View File

@@ -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):

View File

@@ -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<pk>\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'

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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):

View File

@@ -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')

View File

@@ -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',

View File

@@ -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<document_pk>\d+)/versions/upload/interactive/$',
view=UploadInteractiveVersionView.as_view(), name='upload_version'
),
url(
regex=r'^documents/(?P<document_pk>\d+)/pages/append/interactive/(?P<source_id>\d+)/$',
view=DocumentPagesAppendView.as_view(), name='document_pages_append'
),
url(
regex=r'^documents/(?P<document_pk>\d+)/pages/append/interactive/$',
view=DocumentPagesAppendView.as_view(), name='document_pages_append'
),
# Setup views

View File

@@ -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

View File

@@ -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,)

View File

@@ -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

View File

@@ -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