Refactor document page image generation and transformation classes
to cache all transformed versions of a document page.
This commit is contained in:
@@ -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
|
||||
--------
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -16,7 +16,7 @@ function set_image_noninteractive(image) {
|
||||
container.html(html);
|
||||
}
|
||||
|
||||
function load_document_image(image) {
|
||||
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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -15,6 +15,7 @@ from .links import (
|
||||
|
||||
class ConverterApp(MayanAppConfig):
|
||||
name = 'converter'
|
||||
test = True
|
||||
verbose_name = _('Converter')
|
||||
|
||||
def ready(self):
|
||||
|
||||
@@ -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
|
||||
|
||||
0
mayan/apps/converter/tests/__init__.py
Normal file
0
mayan/apps/converter/tests/__init__.py
Normal file
92
mayan/apps/converter/tests/test_classes.py
Normal file
92
mayan/apps/converter/tests/test_classes.py
Normal file
@@ -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
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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({
|
||||
|
||||
42
mayan/apps/documents/migrations/0035_auto_20161102_0633.py
Normal file
42
mayan/apps/documents/migrations/0035_auto_20161102_0633.py
Normal file
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
{% block content_plain %}
|
||||
{% for page in pages %}
|
||||
<img src="{% url 'documents:document_display_print' object.id %}?page={{ page.page_number }}" />
|
||||
<img src="{% url 'rest_api:documentpage-image' page.id %}" />
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
setting_print_size.value
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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<document_id>\d+)/display/$', 'get_document_image', {
|
||||
'size': setting_display_size.value
|
||||
}, 'document_display'
|
||||
),
|
||||
url(
|
||||
r'^(?P<document_id>\d+)/display/print/$', 'get_document_image', {
|
||||
'size': setting_print_size.value
|
||||
}, 'document_display_print'
|
||||
),
|
||||
url(
|
||||
r'^(?P<pk>\d+)/download/form/$',
|
||||
DocumentDownloadFormView.as_view(), name='document_download_form'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
for page in document_pages:
|
||||
for document_page in document_pages:
|
||||
output.append('<div class="carousel-item">')
|
||||
output.append(
|
||||
document_html_widget(
|
||||
page,
|
||||
document_page=document_page,
|
||||
click_view='documents:document_page_view',
|
||||
click_view_arguments=[page.pk],
|
||||
click_view_arguments=(document_page.pk,),
|
||||
fancybox_class='',
|
||||
image_class='lazy-load-carousel',
|
||||
size=setting_display_size.value,
|
||||
@@ -70,31 +65,27 @@ class DocumentPagesCarouselWidget(forms.widgets.Widget):
|
||||
'<div class="carousel-item-page-number">%s</div>' % ugettext(
|
||||
'Page %(page_number)d of %(total_pages)d'
|
||||
) % {
|
||||
'page_number': page.page_number,
|
||||
'page_number': document_page.page_number,
|
||||
'total_pages': total_pages
|
||||
}
|
||||
)
|
||||
output.append('</div>')
|
||||
|
||||
if not total_pages:
|
||||
output.append('<span class="fa-stack fa-lg"><i class="fa fa-file-o fa-stack-2x"></i><i class="fa fa-times fa-stack-1x text-danger"></i></span>')
|
||||
|
||||
output.append('</div>')
|
||||
|
||||
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('<a href="%s">%s</a>' % (
|
||||
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(
|
||||
'<a {gallery_template} class="{fancybox_class}" '
|
||||
'href="{image_data}" {title_template}>'.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(
|
||||
'<img class="thin_border %s" data-src="%s" '
|
||||
'data-post-load-class="%s" src="%s" alt="%s" />' % (
|
||||
'<img class="thin_border {}" data-original="{}" '
|
||||
'data-post-load-class="{}" src="{}" alt="{}" />'.format(
|
||||
image_class, preview_view, post_load_class,
|
||||
static('appearance/images/loading.png'), alt_text
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user