Compare commits

..

39 Commits

Author SHA1 Message Date
Roberto Rosario
4fe6b36069 Add OCR migration dependency
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 12:38:31 -04:00
Roberto Rosario
acd8fd2a3e PEP8 cleanup
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 10:22:42 -04:00
Roberto Rosario
beb3b936a6 Remove the documents app settings
Remove DOCUMENTS_DISABLE_BASE_IMAGE_CACHE,
DOCUMENTS_DISABLE_TRANSFORMED_IMAGE_CACHE, and
DOCUMENTS_FIX_ORIENTATION settings.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 10:22:09 -04:00
Roberto Rosario
f10cc89847 Add document page append view tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 10:10:04 -04:00
Roberto Rosario
01e79b1089 Add OCR migration dependency
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 09:08:51 -04:00
Roberto Rosario
5ea286d4bd Show page append link if new versions are allowed
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-11 09:07:48 -04:00
Roberto Rosario
d865c60091 Update changelog
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 17:18:12 -04:00
Roberto Rosario
4afe81f306 Update document version upload to use dropzone
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 17:16:16 -04:00
Roberto Rosario
126dcfd609 Split source multiform template
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 17:15:45 -04:00
Roberto Rosario
77e3847025 Add directional migration dependency
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 16:32:27 -04:00
Roberto Rosario
4f93beae74 Fix base class name
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 15:52:35 -04:00
Roberto Rosario
af1eae8c52 Merge branch 'versions/minor' into features/multi_version_document
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 14:56:47 -04:00
Roberto Rosario
c731ab7050 Add kwargs and update string formatting
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 14:50:26 -04:00
Roberto Rosario
bd0d298be3 New document version improvements from clients/bc
- Comment field help text.
- Remove create_document_form_form.
- Use static NewVersionForm.
- Update sources document upload and new version upload view names.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 14:34:50 -04:00
Roberto Rosario
739d496799 Add dedicated pages append action
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 11:53:40 -04:00
Roberto Rosario
ff03ea07ca Add support for appending pages
Add version upload form checkbox.
Add the append_pages keyword argument.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 02:40:45 -04:00
Roberto Rosario
03379ab8ec Fix parsing tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-10 01:14:46 -04:00
Roberto Rosario
a4a12b0cfe Fix tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 21:09:36 -04:00
Roberto Rosario
cf697d3ea7 Fix tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 21:06:38 -04:00
Roberto Rosario
a9077cb47a Fix document search tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 19:40:08 -04:00
Roberto Rosario
f163dc78d4 Fix search setup
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 17:15:59 -04:00
Roberto Rosario
64abf66f22 Fix tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 16:55:02 -04:00
Roberto Rosario
7fbb94a8ae Migration updates
Squash version page migrations.
Add manual OCR and parsing migrations.
Fix tests.
Page search updates.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 16:38:00 -04:00
Roberto Rosario
d0ee8aba16 Add document pages reset view
Add document version page count update view.
Add tests.
Register permission_document_tools to the Document model.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 11:53:29 -04:00
Roberto Rosario
5b37c7715d Fix document page render
Solve page_number > 1 error.
Add page_all to Document model.
Enable redactions.
Remove unused methods.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-09 00:38:08 -04:00
Roberto Rosario
8cf807899a Initial commit to support page mapping
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 18:45:53 -04:00
Roberto Rosario
cc8147d002 Update requirements and setup
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 15:15:50 -04:00
Roberto Rosario
1b327b99f0 Update run_test Docker command name
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 15:15:08 -04:00
Roberto Rosario
7b3a83ee39 Update GitLab CI to use Python 3 and virtualenv
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 09:48:54 -04:00
Roberto Rosario
4659269349 Invalidate the layer cache in tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 09:45:28 -04:00
Roberto Rosario
517bb4e9a2 Move Makefile versions to variables
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 09:45:20 -04:00
Roberto Rosario
653f55f84a Invalidate the layer cache in tests
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 09:44:39 -04:00
Roberto Rosario
9cf1d44ee7 Move Makefile versions to variables
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-08 09:44:33 -04:00
Roberto Rosario
162cd256e7 Merge branch 'versions/minor' of gitlab.com:mayan-edms/mayan-edms into versions/minor 2019-10-07 16:43:15 -04:00
Roberto Rosario
339b7dd836 Add missing dependencies import
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 16:43:00 -04:00
Roberto Rosario
949c0ab285 Remove empty ine
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-07 10:43:52 -04:00
Roberto Rosario
cb6cb4121f Fix typos
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-06 03:04:45 -04:00
Roberto Rosario
042727aaa9 Update build string
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-05 15:09:46 -04:00
Roberto Rosario
5b304ea742 Bump version to 3.3 beta 1
Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
2019-10-05 15:08:53 -04:00
143 changed files with 2206 additions and 2302 deletions

View File

@@ -63,7 +63,6 @@ job_docker_nightly:
only:
- nightly
- staging
- /^clients\/.+$/
job_documentation_build:
stage: build_documentation
@@ -153,7 +152,7 @@ job_push_python:
- locale-gen en_US.UTF-8
- update-locale LANG=en_US.UTF-8
- export LC_ALL=en_US.UTF-8
- apt-get install -qq curl exiftool gcc ghostscript gnupg1 graphviz libfuse2 libjpeg-dev libmagic1 libpng-dev libtiff-dev poppler-utils libreoffice poppler-utils python-dev python3-dev python-virtualenv tesseract-ocr tesseract-ocr-deu
- apt-get install -qq curl exiftool gcc ghostscript gnupg1 graphviz libfuse2 libjpeg-dev libmagic1 libpng-dev libtiff-dev poppler-utils libreoffice poppler-utils python-dev python-virtualenv python3-dev tesseract-ocr tesseract-ocr-deu
- virtualenv venv -p /usr/bin/python3
- . venv/bin/activate
- pip install -r requirements.txt -r requirements/testing-base.txt
@@ -163,7 +162,6 @@ job_push_python:
- releases/python
- staging
- nightly
- /^clients\/.+$/
test-mysql:
<<: *test_base

View File

@@ -1,20 +0,0 @@
- Use Select2 widget for the document type selection form.
- Update source column matching to be additive and not exclusive.
- Add two columns to show the number of documents per workflow and
workflow state.
- Sort module.
- Add link to sort individual indexes.
- Support exclusions from source columns.
- Improve source column exclusion. Improve for model subclasses in partial querysets.
- Add sortable index instance label column.
- Add rectangle drawing transformation.
- Redactions app.
- Remove duplicated trashed document preview.
- Add label to trashed date and time document source column.
- Tag created event fix.
3.2.3 (2019-06-21)
* Add a reusable task to upload documents.
* Add MVP of the importer app.
3.2.4-3.2.8 (2019-10-07)

View File

@@ -78,6 +78,11 @@
Support Docker networks and make it the default.
Delete the containers to allow the script to be idempotent.
Deploy a Redis container.
- Improve document version upload form.
- Use dropzone for document version upload form.
- Remove the DOCUMENTS_DISABLE_BASE_IMAGE_CACHE,
DOCUMENTS_DISABLE_TRANSFORMED_IMAGE_CACHE, and
DOCUMENTS_FIX_ORIENTATION settings.
3.2.8 (2019-10-01)
==================

View File

@@ -19,6 +19,7 @@ Changes
GitLab issue #625. Thanks to Jesaja Everling (@jeverling)
for the report and the research.
Removals
--------

View File

@@ -12,8 +12,7 @@ Changes
- Fix help text of the platformtemplate command.
- Fix IMAP4 mailbox.store flags argument. Python's documentation
incorrectly state it is named flag_list. Closes GitLab issue
#606. Thanks to Samuel Aebi (@samuelaebi) for the report and
debug information.
#606. Thanks to Samuel Aebi (@samuelaebi) for the report and
debug information.
- Support configurable GUnicorn timeouts. Defaults to
current value of 120 seconds.

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-9-g1b327b99f0_Tue Oct 8 15:15:08 2019 -0400'
__django_version__ = '1.11'
__author__ = 'Roberto Rosario'
__author_email__ = 'roberto.rosario@mayan-edms.com'

View File

@@ -1,19 +0,0 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.events.classes import EventTypeNamespace
namespace = EventTypeNamespace(
label=_('Authentication'), name='authentication'
)
event_user_authentication_error = namespace.add_event_type(
label=_('User authentication error'), name='user_authentication_error'
)
event_user_password_reset_started = namespace.add_event_type(
label=_('User password reset started'), name='user_password_reset_started'
)
event_user_password_reset_complete = namespace.add_event_type(
label=_('User password reset complete'), name='user_password_reset_complete'
)

View File

@@ -1,82 +0,0 @@
from __future__ import unicode_literals
from django.conf import settings
from django.contrib.auth.views import (
INTERNAL_RESET_SESSION_TOKEN, INTERNAL_RESET_URL_TOKEN,
)
from django.core import mail
from actstream.models import Action
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.events.utils import create_system_user
from ..events import (
event_user_authentication_error, event_user_password_reset_complete,
event_user_password_reset_started
)
class AuthenticationEventsTestCase(GenericViewTestCase):
auto_login_user = False
def setUp(self):
super(AuthenticationEventsTestCase, self).setUp()
create_system_user()
def test_user_authentication_failure_event(self):
Action.objects.all().delete()
response = self.post(viewname=settings.LOGIN_URL)
self.assertEqual(response.status_code, 200)
action = Action.objects.last()
self.assertEqual(action.verb, event_user_authentication_error.id)
def test_user_password_reset_started_event(self):
Action.objects.all().delete()
response = self.post(
viewname='authentication:password_reset_view', data={
'email': self._test_case_user.email,
}
)
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
action = Action.objects.last()
self.assertEqual(action.verb, event_user_password_reset_started.id)
def test_user_password_reset_complete_event(self):
response = self.post(
viewname='authentication:password_reset_view', data={
'email': self._test_case_user.email,
}
)
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
email_parts = mail.outbox[0].body.replace('\n', '').split('/')
uidb64 = email_parts[-3]
token = email_parts[-2]
# Add the token to the session
session = self.client.session
session[INTERNAL_RESET_SESSION_TOKEN] = token
session.save()
Action.objects.all().delete()
new_password = 'new_password_123'
response = self.post(
viewname='authentication:password_reset_confirm_view',
kwargs={'uidb64': uidb64, 'token': INTERNAL_RESET_URL_TOKEN}, data={
'new_password1': new_password,
'new_password2': new_password
}
)
self.assertNotIn(INTERNAL_RESET_SESSION_TOKEN, self.client.session)
action = Action.objects.last()
self.assertEqual(action.verb, event_user_password_reset_complete.id)

View File

@@ -21,13 +21,8 @@ from mayan.apps.common.generics import MultipleObjectFormActionView
from mayan.apps.common.settings import (
setting_home_view, setting_project_title, setting_project_url
)
from mayan.apps.events.utils import get_system_user
from mayan.apps.user_management.permissions import permission_user_edit
from .events import (
event_user_authentication_error, event_user_password_reset_complete,
event_user_password_reset_started
)
from .forms import EmailAuthenticationForm, UsernameAuthenticationForm
from .settings import setting_login_method, setting_maximum_session_length
@@ -62,10 +57,6 @@ class MayanLoginView(StrongholdPublicMixin, LoginView):
return result
def form_invalid(self, form):
event_user_authentication_error.commit(actor=get_system_user())
return super(MayanLoginView, self).form_invalid(form=form)
def get_form_class(self):
if setting_login_method.value == 'email':
return EmailAuthenticationForm
@@ -121,10 +112,6 @@ class MayanPasswordResetConfirmView(StrongholdPublicMixin, PasswordResetConfirmV
)
template_name = 'authentication/password_reset_confirm.html'
def post(self, *args, **kwargs):
event_user_password_reset_complete.commit(actor=get_system_user())
return super(MayanPasswordResetConfirmView, self).post(*args, **kwargs)
class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView):
extra_context = {
@@ -150,10 +137,6 @@ class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView):
)
template_name = 'authentication/password_reset_form.html'
def post(self, *args, **kwargs):
event_user_password_reset_started.commit(actor=get_system_user())
return super(MayanPasswordResetView, self).post(*args, **kwargs)
class UserSetPasswordView(MultipleObjectFormActionView):
form_class = SetPasswordForm

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

@@ -33,7 +33,7 @@ class CabinetDocumentUploadTestCase(CabinetTestMixin, GenericDocumentViewTestCas
def _request_upload_interactive_document_create_view(self):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
return self.post(
viewname='sources:upload_interactive', kwargs={
viewname='sources:document_upload_interactive', kwargs={
'source_id': self.test_source.pk
}, data={
'document_type_id': self.test_document_type.pk,

View File

@@ -376,7 +376,7 @@ class NewVersionBlockViewTestCase(
self.login_superuser()
response = self.post(
viewname='sources:upload_version', kwargs={
viewname='sources:document_version_upload', kwargs={
'document_pk': self.test_document.pk
}, follow=True
)

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:
@@ -66,17 +66,17 @@ class MultiFormView(DjangoFormView):
def dispatch(self, request, *args, **kwargs):
form_classes = self.get_form_classes()
self.forms = self.get_forms(form_classes)
self.forms = self.get_forms(form_classes=form_classes)
return super(MultiFormView, self).dispatch(request, *args, **kwargs)
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)
return getattr(self, form_valid_method)(form=form)
self.all_forms_valid(forms)
self.all_forms_valid(forms=forms)
return HttpResponseRedirect(redirect_to=self.get_success_url())
@@ -98,14 +98,16 @@ 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({
'data': self.request.POST,
'files': self.request.FILES,
})
kwargs.update(
{
'data': self.request.POST,
'files': self.request.FILES,
}
)
kwargs.update(self.get_form_extra_kwargs(form_name=form_name) or {})
@@ -118,13 +120,13 @@ class MultiFormView(DjangoFormView):
return dict(
[
(
key, self._create_form(key, klass)
key, self._create_form(form_name=key, klass=klass)
) for key, klass in form_classes.items()
]
)
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:
@@ -206,9 +208,9 @@ class AddRemoveView(
getattr(self.main_object, self.related_field).add(*queryset)
else:
raise ImproperlyConfigured(
'View %s must be called with a main_object_method_add, a '
'View {} must be called with a main_object_method_add, a '
'related_field, or an action_add '
'method.' % self.__class__.__name__
'method.'.format(self.__class__.__name__)
)
def _action_remove(self, queryset):
@@ -225,9 +227,9 @@ class AddRemoveView(
getattr(self.main_object, self.related_field).remove(*queryset)
else:
raise ImproperlyConfigured(
'View %s must be called with a main_object_method_remove, a '
'View {} must be called with a main_object_method_remove, a '
'related_field, or an action_remove '
'method.' % self.__class__.__name__
'method.'.format(self.__class__.__name__)
)
def dispatch(self, request, *args, **kwargs):
@@ -348,8 +350,10 @@ class AddRemoveView(
def get_list_added_queryset(self):
if not self.related_field:
raise ImproperlyConfigured(
'View %s must be called with either a related_field or '
'override .get_list_added_queryset().' % self.__class__.__name__
'View {} must be called with either a related_field or '
'override .get_list_added_queryset().'.format(
self.__class__.__name__
)
)
return self.get_secondary_object_list().filter(

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

@@ -199,36 +199,3 @@ class IndexToolsViewTestCase(
# An instance root exists
self.assertTrue(self.test_index.instance_root.pk)
def test_index_rebuild_view_no_permission(self):
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(IndexInstanceNode.objects.count(), 0)
def test_index_rebuild_view_with_access(self):
self._create_test_index()
self.test_index.node_templates.create(
parent=self.test_index.template_root,
expression=TEST_INDEX_TEMPLATE_DOCUMENT_LABEL_EXPRESSION,
link_documents=True
)
self.grant_access(
obj=self.test_index,
permission=permission_document_indexing_rebuild
)
response = self._request_test_index_rebuild_view()
self.assertEqual(response.status_code, 302)
self.assertNotEqual(IndexInstanceNode.objects.count(), 0)

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

@@ -12,6 +12,9 @@ class Migration(migrations.Migration):
('documents', '0041_auto_20170823_1855'),
]
run_before = [
('documents', '0052_rename_document_page'),
]
operations = [
migrations.CreateModel(
name='DocumentPageContent',

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

@@ -205,10 +205,10 @@ class DocumentStatesApp(MayanAppConfig):
SourceColumn(
source=WorkflowInstanceLogEntry, label=_('Date and time'),
attribute='datetime', is_sortable=True
attribute='datetime'
)
SourceColumn(
source=WorkflowInstanceLogEntry, attribute='user', is_sortable=True
source=WorkflowInstanceLogEntry, label=_('User'), attribute='user'
)
SourceColumn(
source=WorkflowInstanceLogEntry,

View File

@@ -162,7 +162,6 @@ link_workflow_template_transition_field_delete = Link(
tags='dangerous', text=_('Delete'),
view='document_states:workflow_template_transition_field_delete',
)
link_workflow_template_transition_field_edit = Link(
args='resolved_object.pk',
icon_class_path='mayan.apps.document_states.icons.icon_workflow_transition_field_edit',

View File

@@ -6,11 +6,6 @@ import logging
from furl import furl
from graphviz import Digraph
import yaml
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
from django.apps import apps
from django.conf import settings
@@ -333,8 +328,8 @@ class WorkflowState(models.Model):
def save(self, *args, **kwargs):
# Solve issue #557 "Break workflows with invalid input"
# without using a migration.
# TODO: Remove blank=True, remove this, and create a migration in the
# next minor version.
# Remove blank=True, remove this, and create a migration in the next
# minor version.
try:
self.completion = int(self.completion)

View File

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
from mayan.apps.common.tests.base import GenericViewTestCase
from mayan.apps.documents.tests.base import GenericDocumentViewTestCase
from ..literals import FIELD_TYPE_CHOICE_CHAR
from ..models import WorkflowTransition
from ..permissions import (
permission_workflow_edit, permission_workflow_view,
@@ -20,11 +19,6 @@ from .mixins import (
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin
)
TEST_WORKFLOW_TRANSITION_FIELD_NAME = 'test_workflow_transition_field'
TEST_WORKFLOW_TRANSITION_FIELD_LABEL = 'test workflow transition field'
TEST_WORKFLOW_TRANSITION_FIELD_HELP_TEXT = 'test workflow transition field help test'
TEST_WORKFLOW_TRANSITION_FIELD_TYPE = FIELD_TYPE_CHOICE_CHAR
class WorkflowTransitionViewTestCase(
WorkflowTestMixin, WorkflowViewTestMixin, WorkflowTransitionViewTestMixin,

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
@@ -423,7 +538,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

@@ -35,7 +35,6 @@ DOCUMENT_IMAGES_CACHE_NAME = 'document_images'
DOCUMENT_CACHE_STORAGE_INSTANCE_PATH = 'mayan.apps.documents.storages.storage_documentimagecache'
STUB_EXPIRATION_INTERVAL = 60 * 60 * 24 # 24 hours
UPDATE_PAGE_COUNT_RETRY_DELAY = 10
UPLOAD_NEW_DOCUMENT_RETRY_DELAY = 10
UPLOAD_NEW_VERSION_RETRY_DELAY = 10
PAGE_RANGE_ALL = 'all'
@@ -43,3 +42,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,56 @@
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')
# 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')
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():
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,
@@ -21,12 +23,11 @@ from mayan.apps.converter.utils import get_converter_class
from ..managers import DocumentPageManager
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
from .document_models import Document
__all__ = ('DocumentPage', 'DocumentPageResult')
logger = logging.getLogger(__name__)
@@ -35,16 +36,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 +59,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 +68,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 +77,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 +94,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 +132,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 +193,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 +219,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 +255,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 +296,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

@@ -15,15 +15,13 @@ from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from mayan.apps.converter.exceptions import InvalidOfficeFormat, PageCountError
from mayan.apps.converter.layers import layer_saved_transformations
from mayan.apps.converter.transformations import TransformationRotate
from mayan.apps.converter.utils import get_converter_class
from mayan.apps.mimetype.api import get_mimetype
from ..events import event_document_new_version, event_document_version_revert
from ..literals import DOCUMENT_IMAGES_CACHE_NAME
from ..managers import DocumentVersionManager
from ..settings import setting_fix_orientation, setting_hash_block_size
from ..settings import setting_hash_block_size
from ..signals import post_document_created, post_version_upload
from ..storages import storage_documentversion
@@ -152,15 +150,6 @@ class DocumentVersion(models.Model):
"""
return self.file.storage.exists(self.file.name)
def fix_orientation(self):
for page in self.pages.all():
degrees = page.detect_orientation()
if degrees:
layer_saved_transformations.add_to_object(
obj=page, transformation=TransformationRotate,
arguments='{{"degrees": {}}}'.format(360 - degrees)
)
def get_absolute_url(self):
return reverse(
viewname='documents:document_version_view', kwargs={
@@ -246,23 +235,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 +263,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,10 +283,8 @@ 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()
logger.info(
'New document version "%s" created for document: %s',
@@ -337,6 +314,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 +395,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,271 @@
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_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)
@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
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')
@property
def is_in_trash(self):
return self.document_version.document.is_in_trash
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')
@@ -78,7 +86,3 @@ queue_uploads.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_scan_duplicates_for',
label=_('Scan document duplicates')
)
queue_uploads.add_task_type(
dotted_path='mayan.apps.documents.tasks.task_upload_new_document',
label=_('Upload new document')
)

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

@@ -38,21 +38,6 @@ setting_documentimagecache_storage_arguments = namespace.add_setting(
'Arguments to pass to the DOCUMENT_CACHE_STORAGE_BACKEND.'
),
)
setting_disable_base_image_cache = namespace.add_setting(
global_name='DOCUMENTS_DISABLE_BASE_IMAGE_CACHE', default=False,
help_text=_(
'Disables the first cache tier which stores high resolution, '
'non transformed versions of documents\'s pages.'
)
)
setting_disable_transformed_image_cache = namespace.add_setting(
global_name='DOCUMENTS_DISABLE_TRANSFORMED_IMAGE_CACHE', default=False,
help_text=_(
'Disables the second cache tier which stores medium to low '
'resolution, transformed (rotated, zoomed, etc) versions '
'of documents\' pages.'
)
)
setting_display_height = namespace.add_setting(
global_name='DOCUMENTS_DISPLAY_HEIGHT', default=''
)
@@ -65,15 +50,6 @@ setting_favorite_count = namespace.add_setting(
'Maximum number of favorite documents to remember per user.'
)
)
setting_fix_orientation = namespace.add_setting(
global_name='DOCUMENTS_FIX_ORIENTATION', default=False,
help_text=_(
'Detect the orientation of each of the document\'s pages '
'and create a corresponding rotation transformation to '
'display it rightside up. This is an experimental '
'feature and it is disabled by default.'
)
)
setting_hash_block_size = namespace.add_setting(
global_name='DOCUMENTS_HASH_BLOCK_SIZE',
default=DEFAULT_DOCUMENTS_HASH_BLOCK_SIZE, help_text=_(

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,7 +9,7 @@ from django.db import OperationalError
from mayan.celery import app
from .literals import (
UPDATE_PAGE_COUNT_RETRY_DELAY, UPLOAD_NEW_DOCUMENT_RETRY_DELAY,
RETRY_DELAY_DOCUMENT_RESET_PAGES, UPDATE_PAGE_COUNT_RETRY_DELAY,
UPLOAD_NEW_VERSION_RETRY_DELAY
)
@@ -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(
@@ -122,62 +157,8 @@ def task_update_page_count(self, version_id):
raise self.retry(exc=exception)
@app.task(bind=True, default_retry_delay=UPLOAD_NEW_DOCUMENT_RETRY_DELAY, ignore_result=True)
def task_upload_new_document(self, document_type_id, shared_uploaded_file_id):
DocumentType = apps.get_model(
app_label='documents', model_name='DocumentType'
)
SharedUploadedFile = apps.get_model(
app_label='common', model_name='SharedUploadedFile'
)
try:
document_type = DocumentType.objects.get(pk=document_type_id)
shared_file = SharedUploadedFile.objects.get(
pk=shared_uploaded_file_id
)
except OperationalError as exception:
logger.warning(
'Operational error during attempt to retrieve shared data for '
'new document of type: %s; %s. Retrying.', document_type, exception
)
raise self.retry(exc=exception)
try:
with shared_file.open() as file_object:
document_type.new_document(file_object=file_object)
except OperationalError as exception:
logger.warning(
'Operational error during attempt to create new document '
'of type: %s; %s. Retrying.', document_type, exception
)
raise self.retry(exc=exception)
except Exception as exception:
# This except and else block emulate a finally:
logger.error(
'Unexpected error during attempt to create new document '
'of type: %s; %s', document_type, exception
)
try:
shared_file.delete()
except OperationalError as exception:
logger.warning(
'Operational error during attempt to delete shared '
'file: %s; %s.', shared_file, exception
)
else:
try:
shared_file.delete()
except OperationalError as exception:
logger.warning(
'Operational error during attempt to delete shared '
'file: %s; %s.', shared_file, exception
)
@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 +193,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,7 +20,7 @@ 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 ..links import link_document_pages_reset
from ..models import Document, DocumentPage
from ..permissions import permission_document_edit, permission_document_view
from ..settings import (
@@ -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

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
from django.apps import apps
from django.db.models.signals import post_migrate
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.apps import MayanAppConfig
@@ -12,7 +11,6 @@ from mayan.apps.common.menus import (
from mayan.apps.navigation.classes import SourceColumn
from .dependencies import * # NOQA
from .handlers import handler_create_system_user
from .html_widgets import (
ObjectLinkWidget, widget_event_actor_link, widget_event_type_link
)
@@ -103,8 +101,3 @@ class EventsApp(MayanAppConfig):
link_event_types_subscriptions_list, link_current_user_events
), position=50
)
post_migrate.connect(
dispatch_uid='events_create_system_user',
receiver=handler_create_system_user,
)

View File

@@ -1,7 +0,0 @@
from __future__ import unicode_literals
from .utils import create_system_user
def handler_create_system_user(sender, **kwargs):
create_system_user()

View File

@@ -1,23 +0,0 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.auth import get_user_model
def create_system_user():
"""
User account without a password used to attach events that normally
won't have an actor and a target
"""
user, created = get_user_model().objects.get_or_create(
username='system', defaults={
'first_name': 'System', 'is_staff': False
}
)
return user
def get_system_user():
user = get_user_model().objects.get(username='system')
return user

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

@@ -1,3 +0,0 @@
from __future__ import unicode_literals
default_app_config = 'mayan.apps.importer.apps.ImporterApp'

View File

@@ -1,17 +0,0 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.apps import MayanAppConfig
class ImporterApp(MayanAppConfig):
app_namespace = 'importer'
app_url = 'importer'
has_rest_api = False
has_tests = True
name = 'mayan.apps.importer'
verbose_name = _('Importer')
def ready(self):
super(ImporterApp, self).ready()

View File

@@ -1,152 +0,0 @@
from __future__ import unicode_literals
import csv
import time
from django.apps import apps
from django.core import management
from django.core.files import File
from ...tasks import task_upload_new_document
class Command(management.BaseCommand):
help = 'Import documents from a CSV file.'
def add_arguments(self, parser):
parser.add_argument(
'--document_type_column',
action='store', dest='document_type_column', default=0,
help='Column that contains the document type labels. Column '
'numbers start at 0.',
type=int
)
parser.add_argument(
'--document_path_column',
action='store', dest='document_path_column', default=1,
help='Column that contains the path to the document files. Column '
'numbers start at 0.',
type=int
)
parser.add_argument(
'--ignore_errors',
action='store_true', dest='ignore_errors', default=False,
help='Don\'t stop the import process on common errors like '
'incorrect file paths.',
)
parser.add_argument(
'--ignore_rows',
action='store', dest='ignore_rows', default='',
help='Ignore a set of rows. Row numbers must be separated by commas.'
)
parser.add_argument(
'--metadata_pairs_column',
action='store', dest='metadata_pairs_column',
help='Column that contains metadata name and values for the '
'documents. Use the form: <label column>:<value column>. Example: '
'2:5. Separate multiple pairs with commas. Example: 2:5,7:10',
)
parser.add_argument('filelist', nargs='?', help='File list')
def handle(self, *args, **options):
time_start = time.time()
time_last_display = time_start
document_types = {}
uploaded_count = 0
row_count = 0
rows_to_ignore = []
for entry in options['ignore_rows'].split(','):
if entry:
rows_to_ignore.append(int(entry))
DocumentType = apps.get_model(
app_label='documents', model_name='DocumentType'
)
SharedUploadedFile = apps.get_model(
app_label='common', model_name='SharedUploadedFile'
)
if not options['filelist']:
self.stderr.write('Must specify a CSV file path.')
exit(1)
else:
with open(options['filelist'], mode='r') as csv_datafile:
csv_reader = csv.reader(
csv_datafile, delimiter=',', quotechar='"'
)
for row in csv_reader:
# Increase row count here even though start index is 0
# purpose is to avoid losing row number increments on
# exceptions
row_count = row_count + 1
if row_count - 1 not in rows_to_ignore:
try:
with open(row[options['document_path_column']], mode='rb') as file_object:
document_type_label = row[options['document_type_column']]
if document_type_label not in document_types:
self.stdout.write(
'New document type: {}. Creating and caching.'.format(
document_type_label
)
)
document_type, created = DocumentType.objects.get_or_create(
label=document_type_label
)
document_types[document_type_label] = document_type
else:
document_type = document_types[document_type_label]
shared_uploaded_file = SharedUploadedFile.objects.create(
file=File(file_object)
)
extra_data = {}
if options['metadata_pairs_column']:
extra_data['metadata_pairs'] = []
for pair in options['metadata_pairs_column'].split(','):
name, value = pair.split(':')
extra_data['metadata_pairs'].append(
{
'name': row[int(name)],
'value': row[int(value)]
}
)
task_upload_new_document.apply_async(
kwargs=dict(
document_type_id=document_type.pk,
shared_uploaded_file_id=shared_uploaded_file.pk,
extra_data=extra_data
)
)
uploaded_count = uploaded_count + 1
if (time.time() - time_last_display) > 1:
time_last_display = time.time()
self.stdout.write(
'Time: {}s, Files copied and queued: {}, files processed per second: {}'.format(
int(time.time() - time_start),
uploaded_count,
uploaded_count / (time.time() - time_start)
)
)
except (IOError, OSError) as exception:
if not options['ignore_errors']:
raise
else:
self.stderr.write(
'Error processing row: {}; {}.'.format(
row_count - 1, exception
)
)
self.stdout.write(
'Total files copied and queues: {}'.format(uploaded_count)
)
self.stdout.write(
'Total time: {}'.format(time.time() - time_start)
)

View File

@@ -1,10 +0,0 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.documents.queues import queue_uploads
queue_uploads.add_task_type(
dotted_path='mayan.apps.importer.tasks.task_upload_new_document',
label=_('Import new document')
)

View File

@@ -1,93 +0,0 @@
from __future__ import unicode_literals
import logging
from django.apps import apps
from django.db import OperationalError
from django.utils.text import slugify
from mayan.celery import app
from mayan.apps.documents.literals import UPLOAD_NEW_DOCUMENT_RETRY_DELAY
logger = logging.getLogger(__name__)
@app.task(bind=True, default_retry_delay=UPLOAD_NEW_DOCUMENT_RETRY_DELAY, ignore_result=True)
def task_upload_new_document(self, document_type_id, shared_uploaded_file_id, extra_data=None):
DocumentType = apps.get_model(
app_label='documents', model_name='DocumentType'
)
MetadataType = apps.get_model(
app_label='metadata', model_name='MetadataType'
)
SharedUploadedFile = apps.get_model(
app_label='common', model_name='SharedUploadedFile'
)
try:
document_type = DocumentType.objects.get(pk=document_type_id)
shared_file = SharedUploadedFile.objects.get(
pk=shared_uploaded_file_id
)
except OperationalError as exception:
logger.warning(
'Operational error during attempt to retrieve shared data for '
'new document of type ID: %d; %s. Retrying.', document_type_id,
exception
)
raise self.retry(exc=exception)
try:
with shared_file.open() as file_object:
new_document = document_type.new_document(file_object=file_object)
except OperationalError as exception:
logger.warning(
'Operational error during attempt to create new document '
'of type: %s; %s. Retrying.', document_type, exception
)
raise self.retry(exc=exception)
except Exception as exception:
# This except and else block emulate a finally:
logger.error(
'Unexpected error during attempt to create new document '
'of type: %s; %s', document_type, exception
)
try:
shared_file.delete()
except OperationalError as exception:
logger.warning(
'Operational error during attempt to delete shared '
'file: %s; %s.', shared_file, exception
)
else:
if extra_data:
for pair in extra_data.get('metadata_pairs', []):
name = slugify(pair['name']).replace('-', '_')
logger.debug(
'Metadata pair (label, name, value): %s, %s, %s',
pair['name'], name, pair['value']
)
metadata_type, created = MetadataType.objects.get_or_create(
name=name, defaults={'label': pair['name']}
)
if not new_document.document_type.metadata.filter(metadata_type=metadata_type).exists():
logger.debug('Metadata type created')
new_document.document_type.metadata.create(
metadata_type=metadata_type, required=False
)
new_document.metadata.create(
metadata_type=metadata_type, value=pair['value']
)
try:
shared_file.delete()
except OperationalError as exception:
logger.warning(
'Operational error during attempt to delete shared '
'file: %s; %s.', shared_file, exception
)

View File

@@ -1,117 +0,0 @@
from __future__ import unicode_literals
import csv
from django.core import management
from django.utils.encoding import force_bytes, force_text
from mayan.apps.documents.models import DocumentType, Document
from mayan.apps.documents.tests.base import GenericDocumentTestCase
from mayan.apps.documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
from mayan.apps.storage.utils import fs_cleanup, mkstemp
class ImportManagementCommandTestCase(GenericDocumentTestCase):
auto_generate_test_csv_file = True
auto_upload_document = False
random_primary_key_enable = False
test_import_count = 1
def setUp(self):
super(ImportManagementCommandTestCase, self).setUp()
if self.auto_generate_test_csv_file:
self._create_test_csv_file()
def tearDown(self):
self._destroy_test_csv_file()
super(ImportManagementCommandTestCase, self).tearDown()
def _create_test_csv_file(self):
self.test_csv_path = mkstemp()[1]
print('Test CSV file: {}'.format(self.test_csv_path))
with open(self.test_csv_path, mode='w', newline='') as file_object:
filewriter = csv.writer(
file_object, delimiter=',', quotechar='"',
quoting=csv.QUOTE_MINIMAL
)
print(
'Generating test CSV for {} documents'.format(
self.test_import_count
)
)
for times in range(self.test_import_count):
filewriter.writerow(
[
self.test_document_type.label, TEST_SMALL_DOCUMENT_PATH,
'column 2', 'column 3', 'column 4', 'column 5',
'part #', 'value',
]
)
filewriter.writerow(
[
self.test_document_type.label, TEST_SMALL_DOCUMENT_PATH,
'column 2', 'column 3', 'column 4', 'column 5',
'part#', 'value',
]
)
def _destroy_test_csv_file(self):
fs_cleanup(filename=self.test_csv_path)
def test_import_csv_read(self):
self.test_document_type.delete()
management.call_command('import', self.test_csv_path)
self.assertTrue(DocumentType.objects.count() > 0)
self.assertTrue(Document.objects.count() > 0)
def test_import_document_type_column_mapping(self):
self.test_document_type.delete()
management.call_command(
'import', self.test_csv_path, '--document_type_column', '2'
)
self.assertTrue(DocumentType.objects.first().label == 'column 2')
self.assertTrue(Document.objects.count() > 0)
def test_import_document_path_column_mapping(self):
self.test_document_type.delete()
with self.assertRaises(IOError):
management.call_command(
'import', self.test_csv_path, '--document_path_column', '2'
)
def test_import_metadata_column_mapping(self):
self.test_document_type.delete()
management.call_command(
'import', self.test_csv_path, '--metadata_pairs_column', '2:3,4:5',
)
self.assertTrue(DocumentType.objects.count() > 0)
self.assertTrue(Document.objects.count() > 0)
self.assertTrue(Document.objects.first().metadata.count() > 0)
self.assertEqual(
Document.objects.first().metadata.get(
metadata_type__name='column_2'
).value, 'column 3'
)
def test_import_ambiguous_metadata(self):
self.auto_generate_test_csv_file = False
self.test_import_count = 2
self.test_document_type.delete()
management.call_command(
'import', self.test_csv_path, '--metadata_pairs_column', '6:7',
)
self.assertTrue(DocumentType.objects.count() > 0)
self.assertTrue(Document.objects.count() > 0)
self.assertTrue(Document.objects.first().metadata.count() > 0)
self.assertEqual(
Document.objects.first().metadata.get(
metadata_type__name='part'
).value, '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

@@ -34,7 +34,7 @@ class DocumentUploadMetadataTestCase(MetadataTypeTestMixin, GenericDocumentViewT
def test_upload_interactive_with_unicode_metadata(self):
url = URL(
path=reverse(viewname='sources:upload_interactive')
path=reverse(viewname='sources:document_upload_interactive')
)
url.args['metadata0_id'] = self.test_metadata_type.pk
url.args['metadata0_value'] = TEST_METADATA_VALUE_UNICODE
@@ -61,7 +61,7 @@ class DocumentUploadMetadataTestCase(MetadataTypeTestMixin, GenericDocumentViewT
def test_upload_interactive_with_ampersand_metadata(self):
url = URL(
path=reverse(viewname='sources:upload_interactive')
path=reverse(viewname='sources:document_upload_interactive')
)
url.args['metadata0_id'] = self.test_metadata_type.pk
url.args['metadata0_value'] = TEST_METADATA_VALUE_WITH_AMPERSAND

View File

@@ -689,7 +689,7 @@ class SourceColumn(object):
logger.warning(
'No request variable, aborting request resolution'
)
return final_result
return result
current_view_name = get_current_view_name(request=request)
for column in columns:

View File

@@ -108,6 +108,12 @@ def navigation_resolve_menus(context, names, source=None, sort_results=None):
return result
@register.simple_tag(takes_context=True)
def resolve_link(context, link):
# This can be used to resolve links or menus too
return link.resolve(context=context)
@register.simple_tag(takes_context=True)
def navigation_source_column_resolve(context, column):
if column:
@@ -115,9 +121,3 @@ def navigation_source_column_resolve(context, column):
return result
else:
return ''
@register.simple_tag(takes_context=True)
def resolve_link(context, link):
# This can be used to resolve links or menus too
return link.resolve(context=context)

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

@@ -9,6 +9,10 @@ class Migration(migrations.Migration):
('documents', '__first__'),
]
run_before = [
('documents', '0052_rename_document_page'),
]
operations = [
migrations.CreateModel(
name='DocumentVersionOCRError',

Some files were not shown because too many files have changed in this diff Show More