Add support for client side caching of document page images. The time
the images are cached is controlled by the new setting DOCUMENTS_PAGE_IMAGE_CACHE_TIME which defaults to 3600 seconds (1 hour). Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
@@ -19,6 +19,9 @@
|
||||
Whitenoise.
|
||||
- Display error when attempting to recalculate the page count of an empty
|
||||
document (document stub that has no document version).
|
||||
- Add support for client side caching of document page images. The time
|
||||
the images are cached is controlled by the new setting
|
||||
DOCUMENTS_PAGE_IMAGE_CACHE_TIME which defaults to 3600 seconds (1 hour).
|
||||
|
||||
3.0.1 (2018-07-08)
|
||||
=================
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.decorators.cache import cache_control, patch_cache_control
|
||||
|
||||
from django_downloadview import DownloadMixin, VirtualFile
|
||||
from rest_framework import generics, status
|
||||
@@ -34,6 +35,7 @@ from .serializers import (
|
||||
RecentDocumentSerializer, WritableDocumentSerializer,
|
||||
WritableDocumentTypeSerializer, WritableDocumentVersionSerializer
|
||||
)
|
||||
from .settings import settings_document_page_image_cache_time
|
||||
from .storages import storage_documentimagecache
|
||||
from .tasks import task_generate_document_page_image
|
||||
|
||||
@@ -147,93 +149,6 @@ class APIDocumentListView(generics.ListCreateAPIView):
|
||||
serializer.save(_user=self.request.user)
|
||||
|
||||
|
||||
class APIDocumentVersionDownloadView(DownloadMixin, generics.RetrieveAPIView):
|
||||
"""
|
||||
get: Download a document version.
|
||||
"""
|
||||
lookup_url_kwarg = 'version_pk'
|
||||
|
||||
def get_document(self):
|
||||
document = get_object_or_404(Document, pk=self.kwargs['pk'])
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=(permission_document_download,), user=self.request.user,
|
||||
obj=document
|
||||
)
|
||||
return document
|
||||
|
||||
def get_encoding(self):
|
||||
return self.get_object().encoding
|
||||
|
||||
def get_file(self):
|
||||
preserve_extension = self.request.GET.get(
|
||||
'preserve_extension', self.request.POST.get(
|
||||
'preserve_extension', False
|
||||
)
|
||||
)
|
||||
|
||||
preserve_extension = preserve_extension == 'true' or preserve_extension == 'True'
|
||||
|
||||
instance = self.get_object()
|
||||
return VirtualFile(
|
||||
instance.file, name=instance.get_rendered_string(
|
||||
preserve_extension=preserve_extension
|
||||
)
|
||||
)
|
||||
|
||||
def get_mimetype(self):
|
||||
return self.get_object().mimetype
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
def get_serializer_class(self):
|
||||
return None
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_document().versions.all()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
return self.render_to_response()
|
||||
|
||||
|
||||
class APIDocumentView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Returns the selected document details.
|
||||
delete: Move the selected document to the thrash.
|
||||
get: Return the details of the selected document.
|
||||
patch: Edit the properties of the selected document.
|
||||
put: Edit the properties of the selected document.
|
||||
"""
|
||||
mayan_object_permissions = {
|
||||
'GET': (permission_document_view,),
|
||||
'PUT': (permission_document_properties_edit,),
|
||||
'PATCH': (permission_document_properties_edit,),
|
||||
'DELETE': (permission_document_trash,)
|
||||
}
|
||||
permission_classes = (MayanPermission,)
|
||||
queryset = Document.objects.all()
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
if not self.request:
|
||||
return None
|
||||
|
||||
return super(APIDocumentView, self).get_serializer(*args, **kwargs)
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {
|
||||
'format': self.format_kwarg,
|
||||
'request': self.request,
|
||||
'view': self
|
||||
}
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == 'GET':
|
||||
return DocumentSerializer
|
||||
else:
|
||||
return WritableDocumentSerializer
|
||||
|
||||
|
||||
class APIDocumentPageImageView(generics.RetrieveAPIView):
|
||||
"""
|
||||
get: Returns an image representation of the selected document.
|
||||
@@ -267,6 +182,7 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
|
||||
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')
|
||||
@@ -289,7 +205,12 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
|
||||
|
||||
cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT)
|
||||
with storage_documentimagecache.open(cache_filename) as file_object:
|
||||
return HttpResponse(file_object.read(), content_type='image')
|
||||
response = HttpResponse(file_object.read(), content_type='image')
|
||||
if '_hash' in request.GET:
|
||||
patch_cache_control(
|
||||
response, max_age=settings_document_page_image_cache_time
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
class APIDocumentPageView(generics.RetrieveUpdateAPIView):
|
||||
@@ -395,6 +316,93 @@ class APIDocumentTypeDocumentListView(generics.ListAPIView):
|
||||
return document_type.documents.all()
|
||||
|
||||
|
||||
class APIDocumentVersionDownloadView(DownloadMixin, generics.RetrieveAPIView):
|
||||
"""
|
||||
get: Download a document version.
|
||||
"""
|
||||
lookup_url_kwarg = 'version_pk'
|
||||
|
||||
def get_document(self):
|
||||
document = get_object_or_404(Document, pk=self.kwargs['pk'])
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=(permission_document_download,), user=self.request.user,
|
||||
obj=document
|
||||
)
|
||||
return document
|
||||
|
||||
def get_encoding(self):
|
||||
return self.get_object().encoding
|
||||
|
||||
def get_file(self):
|
||||
preserve_extension = self.request.GET.get(
|
||||
'preserve_extension', self.request.POST.get(
|
||||
'preserve_extension', False
|
||||
)
|
||||
)
|
||||
|
||||
preserve_extension = preserve_extension == 'true' or preserve_extension == 'True'
|
||||
|
||||
instance = self.get_object()
|
||||
return VirtualFile(
|
||||
instance.file, name=instance.get_rendered_string(
|
||||
preserve_extension=preserve_extension
|
||||
)
|
||||
)
|
||||
|
||||
def get_mimetype(self):
|
||||
return self.get_object().mimetype
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
def get_serializer_class(self):
|
||||
return None
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_document().versions.all()
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
return self.render_to_response()
|
||||
|
||||
|
||||
class APIDocumentView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Returns the selected document details.
|
||||
delete: Move the selected document to the thrash.
|
||||
get: Return the details of the selected document.
|
||||
patch: Edit the properties of the selected document.
|
||||
put: Edit the properties of the selected document.
|
||||
"""
|
||||
mayan_object_permissions = {
|
||||
'GET': (permission_document_view,),
|
||||
'PUT': (permission_document_properties_edit,),
|
||||
'PATCH': (permission_document_properties_edit,),
|
||||
'DELETE': (permission_document_trash,)
|
||||
}
|
||||
permission_classes = (MayanPermission,)
|
||||
queryset = Document.objects.all()
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
if not self.request:
|
||||
return None
|
||||
|
||||
return super(APIDocumentView, self).get_serializer(*args, **kwargs)
|
||||
|
||||
def get_serializer_context(self):
|
||||
return {
|
||||
'format': self.format_kwarg,
|
||||
'request': self.request,
|
||||
'view': self
|
||||
}
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == 'GET':
|
||||
return DocumentSerializer
|
||||
else:
|
||||
return WritableDocumentSerializer
|
||||
|
||||
|
||||
class APIRecentDocumentListView(generics.ListAPIView):
|
||||
"""
|
||||
get: Return a list of the recent documents for the current user.
|
||||
|
||||
@@ -5,6 +5,8 @@ import logging
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from furl import furl
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files import File
|
||||
from django.core.files.base import ContentFile
|
||||
@@ -234,10 +236,10 @@ class Document(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse('documents:document_preview', args=(self.pk,))
|
||||
|
||||
def get_api_image_url(self):
|
||||
def get_api_image_url(self, *args, **kwargs):
|
||||
latest_version = self.latest_version
|
||||
if latest_version:
|
||||
return latest_version.get_api_image_url()
|
||||
return latest_version.get_api_image_url(*args, **kwargs)
|
||||
else:
|
||||
return '#'
|
||||
|
||||
@@ -454,10 +456,10 @@ class DocumentVersion(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse('documents:document_version_view', args=(self.pk,))
|
||||
|
||||
def get_api_image_url(self):
|
||||
def get_api_image_url(self, *args, **kwargs):
|
||||
first_page = self.pages.first()
|
||||
if first_page:
|
||||
return first_page.get_api_image_url()
|
||||
return first_page.get_api_image_url(*args, **kwargs)
|
||||
else:
|
||||
return '#'
|
||||
|
||||
@@ -774,6 +776,67 @@ class DocumentPage(models.Model):
|
||||
return self.document_version.document
|
||||
|
||||
def generate_image(self, *args, **kwargs):
|
||||
transformation_list = self.get_combined_transformation_list(*args, **kwargs)
|
||||
|
||||
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 not setting_disable_transformed_image_cache.value and storage_documentimagecache.exists(cache_filename):
|
||||
logger.debug(
|
||||
'transformations cache file "%s" found', cache_filename
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
'transformations cache file "%s" not found', cache_filename
|
||||
)
|
||||
image = self.get_image(transformations=transformation_list)
|
||||
with storage_documentimagecache.open(cache_filename, 'wb+') as file_object:
|
||||
file_object.write(image.getvalue())
|
||||
|
||||
self.cached_images.create(filename=cache_filename)
|
||||
|
||||
return cache_filename
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('documents:document_page_view', args=(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(
|
||||
'rest_api:documentpage-image', args=(
|
||||
self.document.pk, self.document_version.pk, self.pk
|
||||
)
|
||||
)
|
||||
final_url.args['_hash'] = transformations_hash
|
||||
|
||||
return final_url.tostr()
|
||||
|
||||
|
||||
def get_combined_transformation_list(self, *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', [])
|
||||
|
||||
@@ -815,38 +878,7 @@ class DocumentPage(models.Model):
|
||||
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 not setting_disable_transformed_image_cache.value and storage_documentimagecache.exists(cache_filename):
|
||||
logger.debug(
|
||||
'transformations cache file "%s" found', cache_filename
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
'transformations cache file "%s" not found', cache_filename
|
||||
)
|
||||
image = self.get_image(transformations=transformation_list)
|
||||
with storage_documentimagecache.open(cache_filename, 'wb+') as file_object:
|
||||
file_object.write(image.getvalue())
|
||||
|
||||
self.cached_images.create(filename=cache_filename)
|
||||
|
||||
return cache_filename
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('documents:document_page_view', args=(self.pk,))
|
||||
|
||||
def get_api_image_url(self):
|
||||
return reverse(
|
||||
'rest_api:documentpage-image', args=(
|
||||
self.document.pk, self.document_version.pk, self.pk
|
||||
)
|
||||
)
|
||||
return transformation_list
|
||||
|
||||
def get_image(self, transformations=None):
|
||||
cache_filename = self.cache_filename
|
||||
|
||||
@@ -31,6 +31,9 @@ setting_display_height = namespace.add_setting(
|
||||
setting_display_width = namespace.add_setting(
|
||||
global_name='DOCUMENTS_DISPLAY_WIDTH', default='3600'
|
||||
)
|
||||
settings_document_page_image_cache_time = namespace.add_setting(
|
||||
global_name='DOCUMENTS_PAGE_IMAGE_CACHE_TIME', default='3600'
|
||||
)
|
||||
setting_documentimagecache_storage = namespace.add_setting(
|
||||
global_name='DOCUMENTS_CACHE_STORAGE_BACKEND',
|
||||
default='django.core.files.storage.FileSystemStorage'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
{% load documents_tags %}
|
||||
|
||||
{% extends 'appearance/base_plain.html' %}
|
||||
|
||||
{% block title %}{{ title }}{% endblock title %}
|
||||
@@ -5,7 +7,7 @@
|
||||
{% block content_plain %}
|
||||
{% for page in pages %}
|
||||
<img
|
||||
src="{{ page.get_api_image_url }}?width={{ width }}&height={{ height }}" style="width: 100%;"
|
||||
src="{% get_api_image_url page width=width height=height %}" style="width: 100%;"
|
||||
/>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
{% load documents_tags %}
|
||||
|
||||
<div class="instance-image-widget {{ container_class }}">
|
||||
<div class="spinner-container text-primary"
|
||||
style="height: {% if display_full_height %}100%{% else %}{{ display_height|default:150 }}px{% endif %};"
|
||||
@@ -6,7 +8,7 @@
|
||||
</div>
|
||||
<img
|
||||
class="thin_border {{ image_classes }}"
|
||||
data-url="{{ instance.get_api_image_url }}?width={{ image_width }}&height={{ image_height }}&zoom={{ image_zoom }}&rotation={{ image_rotation }}"
|
||||
data-url="{% get_api_image_url instance width=image_width height=image_height zoom=image_zoom rotation=image_rotation %}"
|
||||
src="#"
|
||||
style="{% if image_max_height %}max-height: {{ image_max_height }}; {% endif %}"
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
{% load documents_tags %}
|
||||
|
||||
<a
|
||||
class="fancybox"
|
||||
{% if disable_title_link %}
|
||||
@@ -5,7 +7,7 @@
|
||||
{% else %}
|
||||
data-caption="<a class='a-caption' href='{{ instance.get_absolute_url }}'>{{ instance }} <i class='fa fa-external-link-alt'></i></a>"
|
||||
{% endif %}
|
||||
href="{{ instance.get_api_image_url }}?width={{ size_preview_width }}&height={{ size_preview_height }}"
|
||||
href="{% get_api_image_url instance width=size_preview_width height=size_preview_height %}"
|
||||
data-type="image"
|
||||
{% if gallery_name %}data-fancybox="{{ gallery_name }}"{% endif %}
|
||||
>
|
||||
|
||||
0
mayan/apps/documents/templatetags/__init__.py
Normal file
0
mayan/apps/documents/templatetags/__init__.py
Normal file
10
mayan/apps/documents/templatetags/documents_tags.py
Normal file
10
mayan/apps/documents/templatetags/documents_tags.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.template import Library
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def get_api_image_url(obj, **kwargs):
|
||||
return obj.get_api_image_url(**kwargs)
|
||||
Reference in New Issue
Block a user