diff --git a/docs/releases/2.2.rst b/docs/releases/2.2.rst index e4cb383126..cff2a358e0 100644 --- a/docs/releases/2.2.rst +++ b/docs/releases/2.2.rst @@ -16,6 +16,7 @@ Other changes - Remove dependency on the django-filetransfer library - Fix height calculation in resize transformation - Improve upgrade instructions +- New image caching pipeline Removals -------- diff --git a/mayan/apps/appearance/static/appearance/css/base.css b/mayan/apps/appearance/static/appearance/css/base.css index df2a3566b8..49b1bdeb60 100644 --- a/mayan/apps/appearance/static/appearance/css/base.css +++ b/mayan/apps/appearance/static/appearance/css/base.css @@ -82,8 +82,12 @@ body { overflow-x: scroll; height: 500px; } +#carousel-container img { + width: 100%; +} + .carousel-item { - margin: 5px 10px 10px 10px + margin: 5px 10px 10px 10px; } .carousel-item-page-number { diff --git a/mayan/apps/appearance/static/appearance/js/base.js b/mayan/apps/appearance/static/appearance/js/base.js index c049f84e2d..78a6b55e8a 100644 --- a/mayan/apps/appearance/static/appearance/js/base.js +++ b/mayan/apps/appearance/static/appearance/js/base.js @@ -16,8 +16,8 @@ function set_image_noninteractive(image) { container.html(html); } -function load_document_image(image) { - $.get( image.attr('data-src'), function(result) { +function loadDocumentImage(image) { + $.get(image.attr('data-src'), function(result) { image.attr('src', result.data); image.addClass(image.attr('data-post-load-class')); }) @@ -76,20 +76,11 @@ jQuery(document).ready(function() { e.preventDefault(); }) - $('img.lazy-load').lazyload({ - appear: function(elements_left, settings) { - load_document_image($(this)); - }, - }); + $('img.lazy-load').lazyload(); $('img.lazy-load-carousel').lazyload({ threshold : 400, container: $("#carousel-container"), - appear: function(elements_left, settings) { - var $this = $(this); - $this.removeClass('lazy-load-carousel'); - load_document_image($this); - }, }); $('th input:checkbox').click(function(e) { diff --git a/mayan/apps/converter/__init__.py b/mayan/apps/converter/__init__.py index 8a8f62a335..8578c59ddb 100644 --- a/mayan/apps/converter/__init__.py +++ b/mayan/apps/converter/__init__.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals -from .classes import ( # NOQA - TransformationResize, TransformationRotate, TransformationZoom # NOQA -) +from .classes import ( + BaseTransformation, TransformationResize, TransformationRotate, + TransformationZoom +) # NOQA from .runtime import converter_class # NOQA default_app_config = 'converter.apps.ConverterApp' diff --git a/mayan/apps/converter/apps.py b/mayan/apps/converter/apps.py index f2527f9b50..04e197b661 100644 --- a/mayan/apps/converter/apps.py +++ b/mayan/apps/converter/apps.py @@ -15,6 +15,7 @@ from .links import ( class ConverterApp(MayanAppConfig): name = 'converter' + test = True verbose_name = _('Converter') def ready(self): diff --git a/mayan/apps/converter/classes.py b/mayan/apps/converter/classes.py index 9ac39e5efd..bf151afbbd 100644 --- a/mayan/apps/converter/classes.py +++ b/mayan/apps/converter/classes.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import base64 import logging +from operator import xor import os try: @@ -221,6 +222,26 @@ class BaseTransformation(object): _registry = {} + @staticmethod + def encode_hash(decoded_value): + return hex(abs(decoded_value))[2:] + + @staticmethod + def decode_hash(encoded_value): + return int(encoded_value, 16) + + @staticmethod + def combine(transformations): + result = None + + for transformation in transformations: + if not result: + result = BaseTransformation.decode_hash(transformation.cache_hash()) + else: + result ^= BaseTransformation.decode_hash(transformation.cache_hash()) + + return BaseTransformation.encode_hash(result) + @classmethod def register(cls, transformation): cls._registry[transformation.name] = transformation @@ -240,8 +261,17 @@ class BaseTransformation(object): return string_concat(cls.label, ': ', ', '.join(cls.arguments)) def __init__(self, **kwargs): + self.kwargs = {} for argument_name in self.arguments: setattr(self, argument_name, kwargs.get(argument_name)) + self.kwargs[argument_name] = kwargs.get(argument_name) + + def cache_hash(self): + result = unicode.__hash__(self.name) + for key, value in self.kwargs.items(): + result ^= unicode.__hash__(key) ^ str.__hash__(str(value)) + + return BaseTransformation.encode_hash(result) def execute_on(self, image): self.image = image diff --git a/mayan/apps/converter/tests/__init__.py b/mayan/apps/converter/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mayan/apps/converter/tests/test_classes.py b/mayan/apps/converter/tests/test_classes.py new file mode 100644 index 0000000000..6d6146e151 --- /dev/null +++ b/mayan/apps/converter/tests/test_classes.py @@ -0,0 +1,92 @@ +from __future__ import unicode_literals + +from django.test import TestCase + +from ..classes import ( + BaseTransformation, TransformationResize, TransformationRotate, + TransformationZoom +) + +TRANSFORMATION_RESIZE_WIDTH = 123 +TRANSFORMATION_RESIZE_HEIGHT = 528 +TRANSFORMATION_RESIZE_CACHE_HASH = '2cbabd3aaafdaf8f' +TRANSFORMATION_RESIZE_WIDTH_2 = 124 +TRANSFORMATION_RESIZE_HEIGHT_2 = 529 +TRANSFORMATION_RESIZE_CACHE_HASH_2 = '2cbabd3aaafdaf89' +TRANSFORMATION_ROTATE_DEGRESS = 34 +TRANSFORMATION_ROTATE_CACHE_HASH = '2f9d036e13aacb48' +TRANSFORMATION_COMBINED_CACHE_HASH = '44a3b262e18b5d5d' +TRANSFORMATION_ZOOM_PERCENT = 49 +TRANSFORMATION_ZOOM_CACHE_HASH = '47840c3658dc399a' + + +class TransformationTestCase(TestCase): + def test_resize_cache_hashing(self): + # Test if the hash is being generated correctly + transformation = TransformationResize( + width=TRANSFORMATION_RESIZE_WIDTH, + height=TRANSFORMATION_RESIZE_HEIGHT + ) + + self.assertEqual( + transformation.cache_hash(), TRANSFORMATION_RESIZE_CACHE_HASH + ) + + # Test if the hash is being alternated correctly + transformation = TransformationResize( + width=TRANSFORMATION_RESIZE_WIDTH_2, + height=TRANSFORMATION_RESIZE_HEIGHT_2 + ) + + self.assertEqual( + transformation.cache_hash(), TRANSFORMATION_RESIZE_CACHE_HASH_2 + ) + + def test_rotate_cache_hashing(self): + # Test if the hash is being generated correctly + transformation = TransformationRotate( + degrees=TRANSFORMATION_ROTATE_DEGRESS + ) + + self.assertEqual( + transformation.cache_hash(), TRANSFORMATION_ROTATE_CACHE_HASH + ) + + def test_rotate_zoom_hashing(self): + # Test if the hash is being generated correctly + transformation = TransformationZoom( + percent=TRANSFORMATION_ZOOM_PERCENT + ) + + self.assertEqual( + transformation.cache_hash(), TRANSFORMATION_ZOOM_CACHE_HASH + ) + + def test_cache_hash_combining(self): + # Test magic method and hash combining + + transformation_resize = TransformationResize( + width=TRANSFORMATION_RESIZE_WIDTH, + height=TRANSFORMATION_RESIZE_HEIGHT + ) + + transformation_rotate = TransformationRotate( + degrees=TRANSFORMATION_ROTATE_DEGRESS + ) + + transformation_zoom = TransformationZoom( + percent=TRANSFORMATION_ZOOM_PERCENT + ) + + #self.assertEqual( + # #transformation_rotate ^ transformation_resize ^ transformation_zoom, + # transformation_rotate ^ transformation_resize ^ transformation_zoom, + # #transformation_resize ^ transformation_zoom, + # TRANSFORMATION_COMBINED_CACHE_HASH + #) + + self.assertEqual( + BaseTransformation.combine( + (transformation_rotate, transformation_resize, transformation_zoom) + ), TRANSFORMATION_COMBINED_CACHE_HASH + ) diff --git a/mayan/apps/documents/api_views.py b/mayan/apps/documents/api_views.py index b93e386597..e1ff278262 100644 --- a/mayan/apps/documents/api_views.py +++ b/mayan/apps/documents/api_views.py @@ -3,8 +3,10 @@ from __future__ import absolute_import, unicode_literals import logging from django.core.exceptions import PermissionDenied +from django.http import HttpResponse from django.shortcuts import get_object_or_404 +from django_downloadview import DownloadMixin, VirtualFile from rest_framework import generics, status from rest_framework.response import Response @@ -13,6 +15,7 @@ from permissions import Permission from rest_api.filters import MayanObjectPermissionsFilter from rest_api.permissions import MayanPermission +from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT from .models import ( Document, DocumentPage, DocumentType, DocumentVersion, RecentDocument ) @@ -25,13 +28,14 @@ from .permissions import ( permission_document_type_create, permission_document_type_delete, permission_document_type_edit, permission_document_type_view ) +from .runtime import cache_storage_backend from .serializers import ( - DeletedDocumentSerializer, DocumentPageImageSerializer, - DocumentPageSerializer, DocumentSerializer, + DeletedDocumentSerializer, DocumentPageSerializer, DocumentSerializer, DocumentTypeSerializer, DocumentVersionSerializer, DocumentVersionRevertSerializer, NewDocumentSerializer, NewDocumentVersionSerializer, RecentDocumentSerializer ) +from .tasks import task_generate_document_page_image logger = logging.getLogger(__name__) @@ -86,15 +90,6 @@ class APIDeletedDocumentRestoreView(generics.GenericAPIView): return Response(status=status.HTTP_200_OK) -############## -from django_downloadview import VirtualDownloadView -from django_downloadview import VirtualFile -from django_downloadview import DownloadMixin - -#class SingleObjectDownloadView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, VirtualDownloadView, SingleObjectMixin): -# VirtualFile = VirtualFile - - class APIDocumentDownloadView(DownloadMixin, generics.RetrieveAPIView): """ Download the latest version of a document. @@ -228,8 +223,18 @@ class APIDocumentView(generics.RetrieveUpdateDestroyAPIView): class APIDocumentPageImageView(generics.RetrieveAPIView): """ Returns an image representation of the selected document. - size -- 'x' seprated width and height of the desired image representation. - zoom -- Zoom level of the image to be generated, numeric value only. + --- + GET: + omit_serializer: true + parameters: + - name: size + description: 'x' seprated width and height of the desired image representation. + paramType: query + type: number + - name: zoom + description: Zoom level of the image to be generated, numeric value only. + paramType: query + type: number """ mayan_object_permissions = { @@ -238,7 +243,25 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): mayan_permission_attribute_check = 'document' permission_classes = (MayanPermission,) queryset = DocumentPage.objects.all() - serializer_class = DocumentPageImageSerializer + + def get_serializer_class(self): + return None + + def retrieve(self, request, *args, **kwargs): + size = request.GET.get('size') + zoom = request.GET.get('zoom') + rotation = request.GET.get('rotation') + + task = task_generate_document_page_image.apply_async( + kwargs=dict( + document_page_id=self.kwargs['pk'], size=size, zoom=zoom, + rotation=rotation + ) + ) + + cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT) + with cache_storage_backend.open(cache_filename) as file_object: + return HttpResponse(file_object.read(), content_type='image') class APIDocumentPageView(generics.RetrieveUpdateAPIView): diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 87bae4836d..64c18a5beb 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -68,13 +68,13 @@ from .permissions import ( ) # Just import to initialize the search models from .search import document_search, document_page_search # NOQA -from .settings import setting_thumbnail_size +from .settings import setting_display_size, setting_thumbnail_size from .statistics import ( new_documents_per_month, new_document_pages_per_month, new_document_versions_per_month, total_document_per_month, total_document_page_per_month, total_document_version_per_month ) -from .widgets import document_html_widget, document_thumbnail +from .widgets import document_html_widget class DocumentsApp(MayanAppConfig): @@ -151,8 +151,12 @@ class DocumentsApp(MayanAppConfig): SourceColumn( source=Document, label=_('Thumbnail'), - func=lambda context: document_thumbnail( - context['object'], gallery_name='documents:document_list', + func=lambda context: document_html_widget( + document_page=context['object'].latest_version.pages.first(), + click_view='rest_api:documentpage-image', + click_view_arguments_lazy=lambda: (context['object'].latest_version.pages.first().pk,), + click_view_querydict={'size': setting_display_size.value}, + gallery_name='documents:document_list', size=setting_thumbnail_size.value, title=getattr(context['object'], 'label', None), ) @@ -165,8 +169,8 @@ class DocumentsApp(MayanAppConfig): source=DocumentPage, label=_('Thumbnail'), func=lambda context: document_html_widget( document_page=context['object'], - click_view='documents:document_display', - click_view_arguments=(context['object'].document.pk,), + click_view='rest_api:documentpage-image', + click_view_arguments=(context['object'].pk,), gallery_name='documents:document_page_list', preview_click_view='documents:document_page_view', size=setting_thumbnail_size.value, @@ -178,8 +182,8 @@ class DocumentsApp(MayanAppConfig): source=DocumentPageResult, label=_('Thumbnail'), func=lambda context: document_html_widget( document_page=context['object'], - click_view='documents:document_display', - click_view_arguments=(context['object'].document.pk,), + click_view='rest_api:documentpage-image', + click_view_arguments=(context['object'].pk,), gallery_name='documents:document_page_list', preview_click_view='documents:document_page_view', size=setting_thumbnail_size.value, @@ -205,8 +209,11 @@ class DocumentsApp(MayanAppConfig): SourceColumn( source=DeletedDocument, label=_('Thumbnail'), - func=lambda context: document_thumbnail( - context['object'], + func=lambda context: document_html_widget( + document_page=context['object'].latest_version.pages.first(), + click_view='rest_api:documentpage-image', + click_view_arguments_lazy=lambda: (context['object'].latest_version.pages.first().pk,), + click_view_querydict={'size': setting_display_size.value}, gallery_name='documents:delete_document_list', size=setting_thumbnail_size.value, title=getattr(context['object'], 'label', None), @@ -285,7 +292,7 @@ class DocumentsApp(MayanAppConfig): 'documents.tasks.task_clear_image_cache': { 'queue': 'tools' }, - 'documents.tasks.task_get_document_page_image': { + 'documents.tasks.task_generate_document_page_image': { 'queue': 'converter' }, 'documents.tasks.task_update_page_count': { diff --git a/mayan/apps/documents/forms.py b/mayan/apps/documents/forms.py index 793a593988..4ee5f4abd8 100644 --- a/mayan/apps/documents/forms.py +++ b/mayan/apps/documents/forms.py @@ -29,8 +29,8 @@ class DocumentPageForm(DetailForm): model = DocumentPage def __init__(self, *args, **kwargs): - zoom = kwargs.pop('zoom', 100) - rotation = kwargs.pop('rotation', 0) + zoom = kwargs.pop('zoom', None) + rotation = kwargs.pop('rotation', None) super(DocumentPageForm, self).__init__(*args, **kwargs) self.fields['page_image'].initial = self.instance self.fields['page_image'].widget.attrs.update({ diff --git a/mayan/apps/documents/migrations/0035_auto_20161102_0633.py b/mayan/apps/documents/migrations/0035_auto_20161102_0633.py new file mode 100644 index 0000000000..65629625e0 --- /dev/null +++ b/mayan/apps/documents/migrations/0035_auto_20161102_0633.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('documents', '0034_auto_20160509_2321'), + ] + + operations = [ + migrations.CreateModel( + name='DocumentPageCachedImage', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('filename', models.CharField(max_length=128, verbose_name='Filename')), + ], + options={ + 'verbose_name': 'Document page cached image', + 'verbose_name_plural': 'Document page cached images', + }, + ), + migrations.CreateModel( + name='DocumentPageResult', + fields=[ + ], + options={ + 'ordering': ('document_version__document', 'page_number'), + 'verbose_name': 'Document page', + 'proxy': True, + 'verbose_name_plural': 'Document pages', + }, + bases=('documents.documentpage',), + ), + migrations.AddField( + model_name='documentpagecachedimage', + name='document_page', + field=models.ForeignKey(related_name='cached_images', verbose_name='Document page', to='documents.DocumentPage'), + ), + ] diff --git a/mayan/apps/documents/models.py b/mayan/apps/documents/models.py index d0b80dc50d..a8e89701b9 100644 --- a/mayan/apps/documents/models.py +++ b/mayan/apps/documents/models.py @@ -16,8 +16,8 @@ from django.utils.translation import ugettext, ugettext_lazy as _ from acls.models import AccessControlList from common.literals import TIME_DELTA_UNIT_CHOICES from converter import ( - converter_class, TransformationResize, TransformationRotate, - TransformationZoom + converter_class, BaseTransformation, TransformationResize, + TransformationRotate, TransformationZoom ) from converter.exceptions import InvalidOfficeFormat, PageCountError from converter.literals import DEFAULT_ZOOM_LEVEL, DEFAULT_ROTATION @@ -683,15 +683,15 @@ class DocumentPage(models.Model): def document(self): return self.document_version.document - def get_image(self, *args, **kwargs): - as_base64 = kwargs.pop('as_base64', False) - transformations = kwargs.pop('transformations', []) - size = kwargs.pop('size', setting_display_size.value) + def generate_image(self, *args, **kwargs): + # Convert arguments into transformations + transformations = kwargs.get('transformations', []) + size = kwargs.get('size', setting_display_size.value) rotation = int( - kwargs.pop('rotation', DEFAULT_ROTATION) or DEFAULT_ROTATION - ) + kwargs.get('rotation', DEFAULT_ROTATION) or DEFAULT_ROTATION + ) % 360 zoom_level = int( - kwargs.pop('zoom', DEFAULT_ZOOM_LEVEL) or DEFAULT_ZOOM_LEVEL + kwargs.get('zoom', DEFAULT_ZOOM_LEVEL) or DEFAULT_ZOOM_LEVEL ) if zoom_level < setting_zoom_min_level.value: @@ -700,8 +700,54 @@ class DocumentPage(models.Model): if zoom_level > setting_zoom_max_level.value: zoom_level = setting_zoom_max_level.value - rotation = rotation % 360 + # Generate transformation hash + transformation_list = [] + + # Stored transformations first + for stored_transformation in Transformation.objects.get_for_model(self, as_classes=True): + 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 size: + transformation_list.append( + TransformationResize( + **dict(zip(('width', 'height'), (size.split('x')))) + ) + ) + + if zoom_level: + transformation_list.append(TransformationZoom(percent=zoom_level)) + + cache_filename = '{}-{}'.format( + self.cache_filename, BaseTransformation.combine(transformation_list) + ) + + # Check is transformed image is available + logger.debug('transformations cache filename: %s', cache_filename) + + if cache_storage_backend.exists(cache_filename): + logger.debug( + 'transformations cache file "%s" found', cache_filename + ) + else: + image = self.get_image(transformations=transformation_list) + with cache_storage_backend.open(cache_filename, 'wb+') as file_object: + file_object.write(image.getvalue()) + + self.cached_images.create(filename=cache_filename) + + return cache_filename + + def get_image(self, transformations=None): cache_filename = self.cache_filename logger.debug('Page cache filename: %s', cache_filename) @@ -734,33 +780,15 @@ class DocumentPage(models.Model): cache_storage_backend.delete(cache_filename) raise - # Stored transformations - for stored_transformation in Transformation.objects.get_for_model(self, as_classes=True): - converter.transform(transformation=stored_transformation) - - # Interactive transformations for transformation in transformations: converter.transform(transformation=transformation) - if rotation: - converter.transform(transformation=TransformationRotate( - degrees=rotation) - ) - - if size: - converter.transform(transformation=TransformationResize( - **dict(zip(('width', 'height'), (size.split('x'))))) - ) - - if zoom_level: - converter.transform( - transformation=TransformationZoom(percent=zoom_level) - ) - - return converter.get_page(as_base64=as_base64) + return converter.get_page() def invalidate_cache(self): cache_storage_backend.delete(self.cache_filename) + for cached_image in self.cached_images.all(): + cached_image.delete() @property def siblings(self): @@ -777,6 +805,22 @@ class DocumentPage(models.Model): return '{}-{}'.format(self.document_version.uuid, self.pk) +class DocumentPageCachedImage(models.Model): + document_page = models.ForeignKey( + DocumentPage, related_name='cached_images', + verbose_name=_('Document page') + ) + filename = models.CharField(max_length=128, verbose_name=_('Filename')) + + class Meta: + verbose_name = _('Document page cached image') + verbose_name_plural = _('Document page cached images') + + def delete(self, *args, **kwargs): + cache_storage_backend.delete(self.filename) + return super(DocumentPageCachedImage, self).delete(*args, **kwargs) + + class DocumentPageResult(DocumentPage): class Meta: ordering = ('document_version__document', 'page_number') diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py index 8a07cb12d4..95fe32b76e 100644 --- a/mayan/apps/documents/serializers.py +++ b/mayan/apps/documents/serializers.py @@ -4,30 +4,11 @@ from rest_framework import serializers from common.models import SharedUploadedFile -from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT from .models import ( Document, DocumentVersion, DocumentPage, DocumentType, RecentDocument ) from .settings import setting_language -from .tasks import task_get_document_page_image, task_upload_new_version - - -class DocumentPageImageSerializer(serializers.Serializer): - data = serializers.SerializerMethodField() - - def get_data(self, instance): - request = self.context['request'] - size = request.GET.get('size') - zoom = request.GET.get('zoom') - rotation = request.GET.get('rotation') - - task = task_get_document_page_image.apply_async( - kwargs=dict( - document_page_id=instance.pk, size=size, zoom=zoom, - rotation=rotation, as_base64=True - ) - ) - return task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT) +from .tasks import task_upload_new_version class DocumentPageSerializer(serializers.HyperlinkedModelSerializer): diff --git a/mayan/apps/documents/tasks.py b/mayan/apps/documents/tasks.py index 49f00c8fde..70309258ad 100644 --- a/mayan/apps/documents/tasks.py +++ b/mayan/apps/documents/tasks.py @@ -56,14 +56,15 @@ def task_delete_stubs(): logger.info('Finshed') -@app.task(compression='zlib') -def task_get_document_page_image(document_page_id, *args, **kwargs): +@app.task() +def task_generate_document_page_image(document_page_id, *args, **kwargs): DocumentPage = apps.get_model( app_label='documents', model_name='DocumentPage' ) document_page = DocumentPage.objects.get(pk=document_page_id) - return document_page.get_image(*args, **kwargs) + + return document_page.generate_image(*args, **kwargs) @app.task(bind=True, default_retry_delay=UPDATE_PAGE_COUNT_RETRY_DELAY, ignore_result=True) diff --git a/mayan/apps/documents/templates/documents/document_print.html b/mayan/apps/documents/templates/documents/document_print.html index 3ee1bea746..46360f3ec9 100644 --- a/mayan/apps/documents/templates/documents/document_print.html +++ b/mayan/apps/documents/templates/documents/document_print.html @@ -4,6 +4,9 @@ {% block content_plain %} {% for page in pages %} - + {% endfor %} {% endblock %} + + +setting_print_size.value diff --git a/mayan/apps/documents/tests/test_api.py b/mayan/apps/documents/tests/test_api.py index 2fd1a1edde..115b44c784 100644 --- a/mayan/apps/documents/tests/test_api.py +++ b/mayan/apps/documents/tests/test_api.py @@ -9,7 +9,6 @@ from json import loads from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.test import override_settings -from django.utils.six import BytesIO from django_downloadview import assert_download_response from rest_framework import status @@ -23,7 +22,7 @@ from .literals import ( TEST_DOCUMENT_FILENAME, TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_FILENAME, TEST_SMALL_DOCUMENT_PATH, ) -from ..models import Document, DocumentType, HASH_FUNCTION +from ..models import Document, DocumentType class DocumentTypeAPITestCase(APITestCase): diff --git a/mayan/apps/documents/urls.py b/mayan/apps/documents/urls.py index 63af6b320d..57edb77275 100644 --- a/mayan/apps/documents/urls.py +++ b/mayan/apps/documents/urls.py @@ -12,7 +12,6 @@ from .api_views import ( APIDocumentVersionRevertView, APIDocumentVersionView, APIRecentDocumentListView ) -from .settings import setting_print_size, setting_display_size from .views import ( ClearImageCacheView, DeletedDocumentDeleteView, DeletedDocumentDeleteManyView, DeletedDocumentListView, @@ -98,17 +97,6 @@ urlpatterns = patterns( 'document_multiple_update_page_count', name='document_multiple_update_page_count' ), - - url( - r'^(?P\d+)/display/$', 'get_document_image', { - 'size': setting_display_size.value - }, 'document_display' - ), - url( - r'^(?P\d+)/display/print/$', 'get_document_image', { - 'size': setting_print_size.value - }, 'document_display_print' - ), url( r'^(?P\d+)/download/form/$', DocumentDownloadFormView.as_view(), name='document_download_form' diff --git a/mayan/apps/documents/views.py b/mayan/apps/documents/views.py index 384187d655..3508ed1ddd 100644 --- a/mayan/apps/documents/views.py +++ b/mayan/apps/documents/views.py @@ -1,6 +1,5 @@ from __future__ import absolute_import, unicode_literals -import base64 import logging import urlparse @@ -8,7 +7,7 @@ from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied from django.core.urlresolvers import resolve, reverse, reverse_lazy -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponseRedirect from django.shortcuts import render_to_response, get_object_or_404 from django.template import RequestContext from django.utils.http import urlencode @@ -23,9 +22,7 @@ from common.generics import ( SingleObjectEditView, SingleObjectListView ) from common.mixins import MultipleInstanceActionMixin -from converter.literals import ( - DEFAULT_PAGE_NUMBER, DEFAULT_ROTATION, DEFAULT_ZOOM_LEVEL -) +from converter.literals import DEFAULT_ZOOM_LEVEL from converter.models import Transformation from converter.permissions import permission_transformation_delete from permissions import Permission @@ -36,9 +33,7 @@ from .forms import ( DocumentPropertiesForm, DocumentTypeSelectForm, DocumentTypeFilenameForm_create, PrintForm ) -from .literals import ( - DOCUMENT_IMAGE_TASK_TIMEOUT, PAGE_RANGE_RANGE, DEFAULT_ZIP_FILENAME -) +from .literals import PAGE_RANGE_RANGE, DEFAULT_ZIP_FILENAME from .models import ( DeletedDocument, Document, DocumentType, DocumentPage, DocumentTypeFilename, DocumentVersion, RecentDocument @@ -53,13 +48,10 @@ from .permissions import ( permission_document_view, permission_empty_trash ) from .settings import ( - setting_preview_size, setting_rotation_step, setting_zoom_percent_step, + setting_print_size, setting_rotation_step, setting_zoom_percent_step, setting_zoom_max_level, setting_zoom_min_level ) -from .tasks import ( - task_clear_image_cache, task_get_document_page_image, - task_update_page_count -) +from .tasks import task_clear_image_cache, task_update_page_count from .utils import parse_range logger = logging.getLogger(__name__) @@ -277,8 +269,8 @@ class DocumentPageView(SimpleView): ).dispatch(request, *args, **kwargs) def get_extra_context(self): - zoom = int(self.request.GET.get('zoom', DEFAULT_ZOOM_LEVEL)) - rotation = int(self.request.GET.get('rotation', DEFAULT_ROTATION)) + zoom = self.request.GET.get('zoom') + rotation = self.request.GET.get('rotation') document_page_form = DocumentPageForm( instance=self.get_object(), zoom=zoom, rotation=rotation ) @@ -742,37 +734,6 @@ def document_multiple_document_type_edit(request): ) -# TODO: Get rid of this view and convert widget to use API and base64 only images -def get_document_image(request, document_id, size=setting_preview_size.value): - document = get_object_or_404(Document.passthrough, pk=document_id) - try: - Permission.check_permissions(request.user, (permission_document_view,)) - except PermissionDenied: - AccessControlList.objects.check_access( - permission_document_view, request.user, document - ) - - page = int(request.GET.get('page', DEFAULT_PAGE_NUMBER)) - - zoom = int(request.GET.get('zoom', DEFAULT_ZOOM_LEVEL)) - - version = int(request.GET.get('version', document.latest_version.pk)) - - if zoom < setting_zoom_min_level.value: - zoom = setting_zoom_min_level.value - - if zoom > setting_zoom_max_level.value: - zoom = setting_zoom_max_level.value - - rotation = int(request.GET.get('rotation', DEFAULT_ROTATION)) % 360 - - document_page = document.pages.get(page_number=page) - - task = task_get_document_page_image.apply_async(kwargs=dict(document_page_id=document_page.pk, size=size, zoom=zoom, rotation=rotation, as_base64=True, version=version)) - data = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT) - return HttpResponse(base64.b64decode(data.partition('base64,')[2]), content_type='image') - - class DocumentDownloadFormView(FormView): form_class = DocumentDownloadForm model = Document @@ -1279,6 +1240,7 @@ def document_print(request, document_id): 'appearance_type': 'plain', 'object': document, 'pages': pages, + 'size': setting_print_size.value, 'title': _('Print: %s') % document, }, context_instance=RequestContext(request)) else: diff --git a/mayan/apps/documents/widgets.py b/mayan/apps/documents/widgets.py index 8a7b14c384..d996fde790 100644 --- a/mayan/apps/documents/widgets.py +++ b/mayan/apps/documents/widgets.py @@ -6,8 +6,7 @@ from django.core.urlresolvers import reverse from django.utils.html import strip_tags from django.utils.http import urlencode from django.utils.safestring import mark_safe -from django.utils.translation import ugettext -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext, ugettext_lazy as _ from converter.literals import DEFAULT_ROTATION, DEFAULT_ZOOM_LEVEL @@ -17,8 +16,8 @@ from .settings import setting_display_size, setting_thumbnail_size class DocumentPageImageWidget(forms.widgets.Widget): def render(self, name, value, attrs=None): final_attrs = self.build_attrs(attrs) - zoom = final_attrs.get('zoom', 100) - rotation = final_attrs.get('rotation', 0) + zoom = final_attrs.get('zoom') + rotation = final_attrs.get('rotation') if value: output = [] output.append( @@ -46,20 +45,16 @@ class DocumentPagesCarouselWidget(forms.widgets.Widget): 'data-height-difference=200>' ) - try: - document_pages = value.pages.all() - total_pages = value.pages.count() - except AttributeError: - document_pages = [] - total_pages = 0 + document_pages = value.pages.all() + total_pages = value.pages.count() - for page in document_pages: + for document_page in document_pages: output.append('') + if not total_pages: + output.append('') + output.append('') return mark_safe(''.join(output)) -def document_thumbnail(document, **kwargs): - return document_html_widget( - document_page=document.latest_version.pages.first(), - click_view='documents:document_display', **kwargs - ) - - def document_link(document): return mark_safe('%s' % ( document.get_absolute_url(), document) ) -def document_html_widget(document_page, click_view=None, click_view_arguments=None, zoom=DEFAULT_ZOOM_LEVEL, rotation=DEFAULT_ROTATION, gallery_name=None, fancybox_class='fancybox', image_class='lazy-load', title=None, size=setting_thumbnail_size.value, nolazyload=False, post_load_class=None, disable_title_link=False, preview_click_view=None): +def document_html_widget(document_page, click_view=None, click_view_arguments=None, zoom=DEFAULT_ZOOM_LEVEL, rotation=DEFAULT_ROTATION, gallery_name=None, fancybox_class='fancybox', image_class='lazy-load', title=None, size=setting_thumbnail_size.value, nolazyload=False, post_load_class=None, disable_title_link=False, preview_click_view=None, click_view_querydict=None, click_view_arguments_lazy=None): result = [] alt_text = _('Document page image') @@ -151,6 +142,9 @@ def document_html_widget(document_page, click_view=None, click_view_arguments=No title_template = '' if click_view: + if click_view_arguments_lazy: + click_view_arguments = click_view_arguments_lazy() + result.append( ''.format( @@ -158,8 +152,8 @@ def document_html_widget(document_page, click_view=None, click_view_arguments=No fancybox_class=fancybox_class, image_data='%s?%s' % ( reverse( - click_view, args=click_view_arguments or [document.pk] - ), query_string + click_view, args=click_view_arguments + ), urlencode(click_view_querydict or {}) ), title_template=title_template ) @@ -173,8 +167,8 @@ def document_html_widget(document_page, click_view=None, click_view_arguments=No ) else: result.append( - '%s' % ( + '{}'.format( image_class, preview_view, post_load_class, static('appearance/images/loading.png'), alt_text ) diff --git a/mayan/apps/ocr/views.py b/mayan/apps/ocr/views.py index 0fd2709fbb..2c4f237867 100644 --- a/mayan/apps/ocr/views.py +++ b/mayan/apps/ocr/views.py @@ -99,7 +99,6 @@ class DocumentTypeSubmitView(FormView): def form_valid(self, form): count = 0 - print form.cleaned_data for document in form.cleaned_data['document_type'].documents.all(): document.submit_for_ocr() count += 1