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. Whitenoise.
- Display error when attempting to recalculate the page count of an empty - Display error when attempting to recalculate the page count of an empty
document (document stub that has no document version). 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) 3.0.1 (2018-07-08)
================= =================

View File

@@ -4,6 +4,7 @@ import logging
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 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 django_downloadview import DownloadMixin, VirtualFile
from rest_framework import generics, status from rest_framework import generics, status
@@ -34,6 +35,7 @@ from .serializers import (
RecentDocumentSerializer, WritableDocumentSerializer, RecentDocumentSerializer, WritableDocumentSerializer,
WritableDocumentTypeSerializer, WritableDocumentVersionSerializer WritableDocumentTypeSerializer, WritableDocumentVersionSerializer
) )
from .settings import settings_document_page_image_cache_time
from .storages import storage_documentimagecache from .storages import storage_documentimagecache
from .tasks import task_generate_document_page_image from .tasks import task_generate_document_page_image
@@ -147,93 +149,6 @@ class APIDocumentListView(generics.ListCreateAPIView):
serializer.save(_user=self.request.user) 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): class APIDocumentPageImageView(generics.RetrieveAPIView):
""" """
get: Returns an image representation of the selected document. get: Returns an image representation of the selected document.
@@ -267,6 +182,7 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
def get_serializer_class(self): def get_serializer_class(self):
return None return None
@cache_control(private=True)
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
width = request.GET.get('width') width = request.GET.get('width')
height = request.GET.get('height') height = request.GET.get('height')
@@ -289,7 +205,12 @@ class APIDocumentPageImageView(generics.RetrieveAPIView):
cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT) cache_filename = task.get(timeout=DOCUMENT_IMAGE_TASK_TIMEOUT)
with storage_documentimagecache.open(cache_filename) as file_object: 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): class APIDocumentPageView(generics.RetrieveUpdateAPIView):
@@ -395,6 +316,93 @@ class APIDocumentTypeDocumentListView(generics.ListAPIView):
return document_type.documents.all() 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): class APIRecentDocumentListView(generics.ListAPIView):
""" """
get: Return a list of the recent documents for the current user. get: Return a list of the recent documents for the current user.

View File

@@ -5,6 +5,8 @@ import logging
import os import os
import uuid import uuid
from furl import furl
from django.conf import settings from django.conf import settings
from django.core.files import File from django.core.files import File
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
@@ -234,10 +236,10 @@ class Document(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('documents:document_preview', args=(self.pk,)) 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 latest_version = self.latest_version
if latest_version: if latest_version:
return latest_version.get_api_image_url() return latest_version.get_api_image_url(*args, **kwargs)
else: else:
return '#' return '#'
@@ -454,10 +456,10 @@ class DocumentVersion(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('documents:document_version_view', args=(self.pk,)) 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() first_page = self.pages.first()
if first_page: if first_page:
return first_page.get_api_image_url() return first_page.get_api_image_url(*args, **kwargs)
else: else:
return '#' return '#'
@@ -774,6 +776,67 @@ class DocumentPage(models.Model):
return self.document_version.document return self.document_version.document
def generate_image(self, *args, **kwargs): 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 # Convert arguments into transformations
transformations = kwargs.get('transformations', []) transformations = kwargs.get('transformations', [])
@@ -815,38 +878,7 @@ class DocumentPage(models.Model):
if zoom_level: if zoom_level:
transformation_list.append(TransformationZoom(percent=zoom_level)) transformation_list.append(TransformationZoom(percent=zoom_level))
cache_filename = '{}-{}'.format( return transformation_list
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
)
)
def get_image(self, transformations=None): def get_image(self, transformations=None):
cache_filename = self.cache_filename cache_filename = self.cache_filename

View File

@@ -31,6 +31,9 @@ setting_display_height = namespace.add_setting(
setting_display_width = namespace.add_setting( setting_display_width = namespace.add_setting(
global_name='DOCUMENTS_DISPLAY_WIDTH', default='3600' 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( setting_documentimagecache_storage = namespace.add_setting(
global_name='DOCUMENTS_CACHE_STORAGE_BACKEND', global_name='DOCUMENTS_CACHE_STORAGE_BACKEND',
default='django.core.files.storage.FileSystemStorage' default='django.core.files.storage.FileSystemStorage'

View File

@@ -1,3 +1,5 @@
{% load documents_tags %}
{% extends 'appearance/base_plain.html' %} {% extends 'appearance/base_plain.html' %}
{% block title %}{{ title }}{% endblock title %} {% block title %}{{ title }}{% endblock title %}
@@ -5,7 +7,7 @@
{% block content_plain %} {% block content_plain %}
{% for page in pages %} {% for page in pages %}
<img <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 %} {% endfor %}
{% endblock %} {% endblock %}

View File

@@ -1,3 +1,5 @@
{% load documents_tags %}
<div class="instance-image-widget {{ container_class }}"> <div class="instance-image-widget {{ container_class }}">
<div class="spinner-container text-primary" <div class="spinner-container text-primary"
style="height: {% if display_full_height %}100%{% else %}{{ display_height|default:150 }}px{% endif %};" style="height: {% if display_full_height %}100%{% else %}{{ display_height|default:150 }}px{% endif %};"
@@ -6,7 +8,7 @@
</div> </div>
<img <img
class="thin_border {{ image_classes }}" 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="#" src="#"
style="{% if image_max_height %}max-height: {{ image_max_height }}; {% endif %}" style="{% if image_max_height %}max-height: {{ image_max_height }}; {% endif %}"
/> />

View File

@@ -1,3 +1,5 @@
{% load documents_tags %}
<a <a
class="fancybox" class="fancybox"
{% if disable_title_link %} {% if disable_title_link %}
@@ -5,7 +7,7 @@
{% else %} {% else %}
data-caption="<a class='a-caption' href='{{ instance.get_absolute_url }}'>{{ instance }} <i class='fa fa-external-link-alt'></i></a>" data-caption="<a class='a-caption' href='{{ instance.get_absolute_url }}'>{{ instance }} <i class='fa fa-external-link-alt'></i></a>"
{% endif %} {% 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" data-type="image"
{% if gallery_name %}data-fancybox="{{ gallery_name }}"{% endif %} {% 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)