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:
Roberto Rosario
2018-08-08 01:31:21 -04:00
parent 170a83b06f
commit c9bb13f149
9 changed files with 189 additions and 127 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 %}"
/>

View File

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

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