Refactor and implement download code natively

- Use modified port of Django 2.2 FileResponse.
- Remove Django DownloadView library.

Signed-off-by: Roberto Rosario <roberto.rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-12-12 19:38:12 -04:00
parent 826f7fddf2
commit a7b31fc171
24 changed files with 355 additions and 278 deletions

View File

@@ -20,6 +20,8 @@
- Add the ID and the URL to the checkout serializer. - Add the ID and the URL to the checkout serializer.
- Add BaseTransformationType metaclass in a way compatible with - Add BaseTransformationType metaclass in a way compatible with
Python 2 and Python 3. Python 2 and Python 3.
- Remove Django DownloadView library. Implement downloads natively
using modified port of Django 2.2 FileResponse.
3.3.4 (2019-12-09) 3.3.4 (2019-12-09)
================== ==================

View File

@@ -1,8 +1,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os
import types import types
from django.conf import settings
from django.http.response import StreamingHttpResponse
from django.utils import six from django.utils import six
from django.utils.six.moves.urllib.parse import quote
from mayan.apps.mimetype.api import get_mimetype
if six.PY3: if six.PY3:
dict_type = dict dict_type = dict
@@ -22,3 +28,75 @@ except NameError:
FileNotFoundErrorException = IOError FileNotFoundErrorException = IOError
else: else:
FileNotFoundErrorException = FileNotFoundError # NOQA FileNotFoundErrorException = FileNotFoundError # NOQA
class FileResponse(StreamingHttpResponse):
"""
Port of Django's 2.2 FileResponse
Modified to allows downloading non file like content as attachment
A streaming HTTP response class optimized for files.
TODO: To be remove when the code moves to Django 2.2
"""
block_size = 4096
def __init__(self, as_attachment=False, filename='', *args, **kwargs):
self.as_attachment = as_attachment
self.filename = filename
super(FileResponse, self).__init__(*args, **kwargs)
def _set_as_attachment(self, filename):
if self.as_attachment:
filename = self.filename or os.path.basename(filename)
if filename:
try:
filename.encode('ascii')
file_expr = 'filename="{}"'.format(filename)
except UnicodeEncodeError:
file_expr = "filename*=utf-8''{}".format(quote(filename))
self['Content-Disposition'] = 'attachment; {}'.format(file_expr)
def _set_streaming_content(self, value):
if not hasattr(value, 'read'):
self.file_to_stream = None
result = super(FileResponse, self)._set_streaming_content(value)
self._set_as_attachment(filename=self.filename)
return result
self.file_to_stream = filelike = value
if hasattr(filelike, 'close'):
self._closable_objects.append(filelike)
value = iter(lambda: filelike.read(self.block_size), b'')
self.set_headers(filelike)
super(FileResponse, self)._set_streaming_content(value)
def set_headers(self, filelike):
"""
Set some common response headers (Content-Length, Content-Type, and
Content-Disposition) based on the `filelike` response content.
"""
encoding_map = {
'bzip2': 'application/x-bzip',
'gzip': 'application/gzip',
'xz': 'application/x-xz',
}
filename = getattr(filelike, 'name', None)
filename = filename if (isinstance(filename, str) and filename) else self.filename
if os.path.isabs(filename):
self['Content-Length'] = os.path.getsize(filelike.name)
elif hasattr(filelike, 'getbuffer'):
self['Content-Length'] = filelike.getbuffer().nbytes
if self.get('Content-Type', '').startswith(settings.DEFAULT_CONTENT_TYPE):
if self.file_to_stream:
content_type, encoding = get_mimetype(
file_object=self.file_to_stream, mimetype_only=True
)
# Encoding isn't set to prevent browsers from automatically
# uncompressing files.
content_type = encoding_map.get(encoding, content_type)
self['Content-Type'] = content_type or 'application/octet-stream'
else:
self['Content-Type'] = 'application/octet-stream'
self._set_as_attachment(filename=filename)

View File

@@ -11,15 +11,13 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import ( from django.views.generic import (
FormView as DjangoFormView, DetailView, TemplateView FormView as DjangoFormView, DetailView, TemplateView
) )
from django.views.generic.base import View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import ( from django.views.generic.edit import (
CreateView, DeleteView, FormMixin, ModelFormMixin, UpdateView CreateView, DeleteView, FormMixin, ModelFormMixin, UpdateView
) )
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django_downloadview import (
TextIteratorIO, VirtualDownloadView, VirtualFile
)
from pure_pagination.mixins import PaginationMixin from pure_pagination.mixins import PaginationMixin
from mayan.apps.acls.models import AccessControlList from mayan.apps.acls.models import AccessControlList
@@ -36,11 +34,10 @@ from .literals import (
TEXT_SORT_ORDER_VARIABLE_NAME TEXT_SORT_ORDER_VARIABLE_NAME
) )
from .mixins import ( from .mixins import (
DeleteExtraDataMixin, DynamicFormViewMixin, ExternalObjectMixin, DeleteExtraDataMixin, DownloadMixin, DynamicFormViewMixin,
ExtraContextMixin, FormExtraKwargsMixin, MultipleObjectMixin, ExternalObjectMixin, ExtraContextMixin, FormExtraKwargsMixin,
ObjectActionMixin, ObjectNameMixin, MultipleObjectMixin, ObjectActionMixin, ObjectNameMixin,
ObjectPermissionCheckMixin, RedirectionMixin, RestrictedQuerysetMixin, RedirectionMixin, RestrictedQuerysetMixin, ViewPermissionCheckMixin
ViewPermissionCheckMixin
) )
from .settings import setting_paginate_by from .settings import setting_paginate_by
@@ -491,7 +488,9 @@ class MultipleObjectConfirmActionView(
class SimpleView(ViewPermissionCheckMixin, ExtraContextMixin, TemplateView): class SimpleView(ViewPermissionCheckMixin, ExtraContextMixin, TemplateView):
pass """
Basic template view class with permission check and extra context
"""
class SingleObjectCreateView( class SingleObjectCreateView(
@@ -657,9 +656,52 @@ class SingleObjectDetailView(
return super(SingleObjectDetailView, self).get_queryset() return super(SingleObjectDetailView, self).get_queryset()
class SingleObjectDownloadView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, VirtualDownloadView, SingleObjectMixin): class BaseDownloadView(DownloadMixin, ViewPermissionCheckMixin, View):
TextIteratorIO = TextIteratorIO def get(self, request, *args, **kwargs):
VirtualFile = VirtualFile return self.render_to_response()
class SingleObjectDownloadView(
RestrictedQuerysetMixin, SingleObjectMixin, BaseDownloadView
):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super(SingleObjectDownloadView, self).get(
request, *args, **kwargs
)
def get_download_file_object(self):
return self.object.open()
def get_download_label(self):
return force_text(self.object)
class MultipleObjectDownloadView(
RestrictedQuerysetMixin, MultipleObjectMixin, BaseDownloadView
):
"""
View that support receiving multiple objects via a pk_list query.
"""
def __init__(self, *args, **kwargs):
result = super(MultipleObjectDownloadView, self).__init__(*args, **kwargs)
if self.__class__.mro()[0].get_queryset != MultipleObjectDownloadView.get_queryset:
raise ImproperlyConfigured(
'%(cls)s is overloading the get_queryset method. Subclasses '
'should implement the get_source_queryset method instead. ' % {
'cls': self.__class__.__name__
}
)
return result
def get_queryset(self):
try:
return super(MultipleObjectDownloadView, self).get_queryset()
except ImproperlyConfigured:
self.queryset = self.get_source_queryset()
return super(MultipleObjectDownloadView, self).get_queryset()
class SingleObjectDynamicFormCreateView( class SingleObjectDynamicFormCreateView(

View File

@@ -13,6 +13,7 @@ from mayan.apps.acls.classes import ModelPermission
from mayan.apps.acls.models import AccessControlList from mayan.apps.acls.models import AccessControlList
from mayan.apps.permissions import Permission from mayan.apps.permissions import Permission
from .compat import FileResponse
from .exceptions import ActionError from .exceptions import ActionError
from .forms import DynamicForm from .forms import DynamicForm
from .literals import PK_LIST_SEPARATOR from .literals import PK_LIST_SEPARATOR
@@ -56,6 +57,29 @@ class DeleteExtraDataMixin(object):
return HttpResponseRedirect(redirect_to=success_url) return HttpResponseRedirect(redirect_to=success_url)
class DownloadMixin(object):
as_attachment = True
def get_as_attachment(self):
return self.as_attachment
def get_download_file_object(self):
raise NotImplementedError(
'Class must provide a .get_download_file_object() method that '
'return a file like object.'
)
def get_download_filename(self):
return None
def render_to_response(self, **response_kwargs):
return FileResponse(
as_attachment=self.get_as_attachment(),
filename=self.get_download_filename(),
streaming_content=self.get_download_file_object()
)
class DynamicFormViewMixin(object): class DynamicFormViewMixin(object):
form_class = DynamicForm form_class = DynamicForm
@@ -345,26 +369,6 @@ class ObjectNameMixin(object):
return object_name return object_name
# TODO: Remove this mixin and replace with restricted queryset
class ObjectPermissionCheckMixin(object):
object_permission = None
def get_permission_object(self):
return self.get_object()
def dispatch(self, request, *args, **kwargs):
if self.object_permission:
AccessControlList.objects.check_access(
obj=self.get_permission_object(),
permissions=(self.object_permission,),
user=request.user
)
return super(
ObjectPermissionCheckMixin, self
).dispatch(request, *args, **kwargs)
class RedirectionMixin(object): class RedirectionMixin(object):
action_cancel_redirect = None action_cancel_redirect = None
next_url = None next_url = None

View File

@@ -2,8 +2,6 @@ from __future__ import absolute_import, unicode_literals
from django.test import TestCase from django.test import TestCase
from django_downloadview import assert_download_response
from mayan.apps.acls.tests.mixins import ACLTestCaseMixin from mayan.apps.acls.tests.mixins import ACLTestCaseMixin
from mayan.apps.converter.tests.mixins import LayerTestCaseMixin from mayan.apps.converter.tests.mixins import LayerTestCaseMixin
from mayan.apps.permissions.tests.mixins import PermissionTestCaseMixin from mayan.apps.permissions.tests.mixins import PermissionTestCaseMixin
@@ -14,7 +12,7 @@ from mayan.apps.user_management.tests.mixins import UserTestMixin
from .mixins import ( from .mixins import (
ClientMethodsTestCaseMixin, ConnectionsCheckTestCaseMixin, ClientMethodsTestCaseMixin, ConnectionsCheckTestCaseMixin,
ContentTypeCheckTestCaseMixin, ModelTestCaseMixin, ContentTypeCheckTestCaseMixin, DownloadTestCaseMixin, ModelTestCaseMixin,
OpenFileCheckTestCaseMixin, RandomPrimaryKeyModelMonkeyPatchMixin, OpenFileCheckTestCaseMixin, RandomPrimaryKeyModelMonkeyPatchMixin,
SilenceLoggerTestCaseMixin, TempfileCheckTestCasekMixin, SilenceLoggerTestCaseMixin, TempfileCheckTestCasekMixin,
TestViewTestCaseMixin TestViewTestCaseMixin
@@ -22,7 +20,8 @@ from .mixins import (
class BaseTestCase( class BaseTestCase(
LayerTestCaseMixin, SilenceLoggerTestCaseMixin, ConnectionsCheckTestCaseMixin, LayerTestCaseMixin, SilenceLoggerTestCaseMixin,
ConnectionsCheckTestCaseMixin, DownloadTestCaseMixin,
RandomPrimaryKeyModelMonkeyPatchMixin, ACLTestCaseMixin, RandomPrimaryKeyModelMonkeyPatchMixin, ACLTestCaseMixin,
ModelTestCaseMixin, OpenFileCheckTestCaseMixin, PermissionTestCaseMixin, ModelTestCaseMixin, OpenFileCheckTestCaseMixin, PermissionTestCaseMixin,
SmartSettingsTestCaseMixin, TempfileCheckTestCasekMixin, UserTestMixin, SmartSettingsTestCaseMixin, TempfileCheckTestCasekMixin, UserTestMixin,
@@ -31,7 +30,6 @@ class BaseTestCase(
""" """
This is the most basic test case class any test in the project should use. This is the most basic test case class any test in the project should use.
""" """
assert_download_response = assert_download_response
class GenericViewTestCase( class GenericViewTestCase(

View File

@@ -18,12 +18,16 @@ from django.http import HttpResponse
from django.template import Context, Template from django.template import Context, Template
from django.test.utils import ContextList from django.test.utils import ContextList
from django.urls import clear_url_caches, reverse from django.urls import clear_url_caches, reverse
from django.utils.encoding import force_bytes from django.utils.encoding import (
DjangoUnicodeDecodeError, force_bytes, force_text
)
from django.utils.six import PY3 from django.utils.six import PY3
from mayan.apps.acls.classes import ModelPermission from mayan.apps.acls.classes import ModelPermission
from mayan.apps.storage.settings import setting_temporary_directory from mayan.apps.storage.settings import setting_temporary_directory
from ..compat import FileResponse
from .literals import ( from .literals import (
TEST_SERVER_HOST, TEST_SERVER_SCHEME, TEST_VIEW_NAME, TEST_VIEW_URL TEST_SERVER_HOST, TEST_SERVER_SCHEME, TEST_VIEW_NAME, TEST_VIEW_URL
) )
@@ -141,6 +145,37 @@ class ContentTypeCheckTestCaseMixin(object):
self.client = CustomClient() self.client = CustomClient()
class DownloadTestCaseMixin(object):
def assert_download_response(
self, response, content=None, filename=None, is_attachment=None,
mime_type=None
):
self.assertTrue(isinstance(response, FileResponse))
if filename:
self.assertEqual(
response[
'Content-Disposition'
].split('filename="')[1].split('"')[0], filename
)
if content:
response_content = b''.join(list(response))
try:
response_content = force_text(response_content)
except DjangoUnicodeDecodeError:
"""Leave as bytes"""
self.assertEqual(response_content, content)
if is_attachment is not None:
self.assertEqual(response['Content-Disposition'], 'attachment')
if mime_type:
self.assertTrue(response['Content-Type'].startswith(mime_type))
class EnvironmentTestCaseMixin(object): class EnvironmentTestCaseMixin(object):
def setUp(self): def setUp(self):
super(EnvironmentTestCaseMixin, self).setUp() super(EnvironmentTestCaseMixin, self).setUp()

View File

@@ -1,7 +1,5 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django_downloadview.test import assert_download_response
from mayan.apps.common.tests.base import GenericViewTestCase from mayan.apps.common.tests.base import GenericViewTestCase
from ..models import Key from ..models import Key
@@ -16,10 +14,10 @@ class KeyViewTestCase(KeyTestMixin, KeyViewTestMixin, GenericViewTestCase):
self._create_test_key_private() self._create_test_key_private()
response = self._request_test_key_download_view() response = self._request_test_key_download_view()
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 404)
def test_key_download_view_with_permission(self): def test_key_download_view_with_permission(self):
self.expected_content_types = ('application/octet-stream; charset=utf-8',) self.expected_content_types = ('text/html; charset=utf-8',)
self._create_test_key_private() self._create_test_key_private()
@@ -28,9 +26,9 @@ class KeyViewTestCase(KeyTestMixin, KeyViewTestMixin, GenericViewTestCase):
) )
response = self._request_test_key_download_view() response = self._request_test_key_download_view()
assert_download_response( self.assert_download_response(
self, response=response, content=self.test_key_private.key_data, response=response, content=self.test_key_private.key_data,
basename=self.test_key_private.key_id, filename=self.test_key_private.key_id,
) )
def test_key_upload_view_no_permission(self): def test_key_upload_view_no_permission(self):

View File

@@ -3,7 +3,6 @@ from __future__ import absolute_import, unicode_literals
import logging import logging
from django.contrib import messages from django.contrib import messages
from django.core.files.base import ContentFile
from django.template import RequestContext from django.template import RequestContext
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -56,10 +55,11 @@ class KeyDownloadView(SingleObjectDownloadView):
model = Key model = Key
object_permission = permission_key_download object_permission = permission_key_download
def get_file(self): def get_download_file_object(self):
key = self.get_object() return self.object.key_data
return ContentFile(key.key_data, name=key.key_id) def get_download_filename(self):
return self.object.key_id
class KeyReceive(ConfirmView): class KeyReceive(ConfirmView):

View File

@@ -114,12 +114,10 @@ class DocumentContentViewsTestCase(
def test_document_parsing_download_view_no_permission(self): def test_document_parsing_download_view_no_permission(self):
response = self._request_test_document_content_download_view() response = self._request_test_document_content_download_view()
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 404)
def test_document_parsing_download_view_with_access(self): def test_document_parsing_download_view_with_access(self):
self.expected_content_types = ( self.expected_content_types = ('text/html; charset=utf-8',)
'application/octet-stream; charset=utf-8',
)
self.grant_access( self.grant_access(
obj=self.test_document, permission=permission_content_view obj=self.test_document, permission=permission_content_view
) )

View File

@@ -75,13 +75,11 @@ class DocumentContentDownloadView(SingleObjectDownloadView):
model = Document model = Document
object_permission = permission_content_view object_permission = permission_content_view
def get_file(self): def get_download_file_object(self):
file_object = DocumentContentDownloadView.TextIteratorIO( return get_document_content(document=self.object)
iterator=get_document_content(document=self.get_object())
) def get_download_filename(self):
return DocumentContentDownloadView.VirtualFile( return '{}-content'.format(self.object)
file=file_object, name='{}-content'.format(self.get_object())
)
class DocumentPageContentView(SingleObjectDetailView): class DocumentPageContentView(SingleObjectDetailView):

View File

@@ -1,7 +1,5 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django_downloadview.test import assert_download_response
from mayan.apps.django_gpg.permissions import permission_key_sign from mayan.apps.django_gpg.permissions import permission_key_sign
from mayan.apps.django_gpg.tests.mixins import KeyTestMixin from mayan.apps.django_gpg.tests.mixins import KeyTestMixin
from mayan.apps.documents.models import DocumentVersion from mayan.apps.documents.models import DocumentVersion
@@ -287,7 +285,7 @@ class DetachedSignaturesViewTestCase(
self._create_test_detached_signature() self._create_test_detached_signature()
response = self._request_test_document_version_signature_download_view() response = self._request_test_document_version_signature_download_view()
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 404)
def test_signature_download_view_with_access(self): def test_signature_download_view_with_access(self):
self.test_document_path = TEST_SMALL_DOCUMENT_PATH self.test_document_path = TEST_SMALL_DOCUMENT_PATH
@@ -300,13 +298,13 @@ class DetachedSignaturesViewTestCase(
permission=permission_document_version_signature_download permission=permission_document_version_signature_download
) )
self.expected_content_types = ('application/octet-stream; charset=utf-8',) self.expected_content_types = ('application/octet-stream',)
response = self._request_test_document_version_signature_download_view() response = self._request_test_document_version_signature_download_view()
with self.test_signature.signature_file as file_object: with self.test_signature.signature_file as file_object:
assert_download_response( self.assert_download_response(
self, response=response, content=file_object.read(), response=response, content=file_object.read(),
) )
def test_signature_upload_view_no_permission(self): def test_signature_upload_view_no_permission(self):

View File

@@ -254,12 +254,11 @@ class DocumentVersionSignatureDownloadView(SingleObjectDownloadView):
model = DetachedSignature model = DetachedSignature
object_permission = permission_document_version_signature_download object_permission = permission_document_version_signature_download
def get_file(self): def get_download_file_object(self):
signature = self.get_object() return self.object.signature_file
return DocumentVersionSignatureDownloadView.VirtualFile( def get_download_filename(self):
signature.signature_file, name=force_text(signature) return force_text(self.object)
)
class DocumentVersionSignatureListView( class DocumentVersionSignatureListView(

View File

@@ -6,12 +6,12 @@ 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.views.decorators.cache import cache_control, patch_cache_control
from django_downloadview import DownloadMixin, VirtualFile
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from mayan.apps.acls.models import AccessControlList from mayan.apps.acls.models import AccessControlList
from mayan.apps.rest_api import generics from mayan.apps.rest_api import generics
from mayan.apps.common.generics import DownloadMixin
from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT
from .models import ( from .models import (
@@ -113,15 +113,11 @@ class APIDocumentDownloadView(DownloadMixin, generics.RetrieveAPIView):
} }
queryset = Document.objects.all() queryset = Document.objects.all()
def get_encoding(self): def get_download_file_object(self):
return self.get_object().latest_version.encoding return self.get_object().open()
def get_file(self): def get_download_filename(self):
instance = self.get_object() return self.get_object().label
return VirtualFile(instance.latest_version.file, name=instance.label)
def get_mimetype(self):
return self.get_object().latest_version.mimetype
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
return None return None
@@ -349,10 +345,11 @@ class APIDocumentVersionDownloadView(DownloadMixin, generics.RetrieveAPIView):
) )
return document return document
def get_encoding(self): def get_download_file_object(self):
return self.get_object().encoding instance = self.get_object()
return instance.open()
def get_file(self): def get_download_filename(self):
preserve_extension = self.request.GET.get( preserve_extension = self.request.GET.get(
'preserve_extension', self.request.POST.get( 'preserve_extension', self.request.POST.get(
'preserve_extension', False 'preserve_extension', False
@@ -362,15 +359,10 @@ class APIDocumentVersionDownloadView(DownloadMixin, generics.RetrieveAPIView):
preserve_extension = preserve_extension == 'true' or preserve_extension == 'True' preserve_extension = preserve_extension == 'true' or preserve_extension == 'True'
instance = self.get_object() instance = self.get_object()
return VirtualFile( return instance.get_rendered_string(
instance.file, name=instance.get_rendered_string( preserve_extension=preserve_extension
preserve_extension=preserve_extension
)
) )
def get_mimetype(self):
return self.get_object().mimetype
def get_serializer(self, *args, **kwargs): def get_serializer(self, *args, **kwargs):
return None return None

View File

@@ -45,7 +45,9 @@ class DocumentDownloadForm(forms.Form):
super(DocumentDownloadForm, self).__init__(*args, **kwargs) super(DocumentDownloadForm, self).__init__(*args, **kwargs)
if self.queryset.count() > 1: if self.queryset.count() > 1:
self.fields['compressed'].initial = True self.fields['compressed'].initial = True
self.fields['compressed'].widget.attrs.update({'disabled': True}) self.fields['compressed'].widget.attrs.update(
{'disabled': 'disabled'}
)
class DocumentForm(forms.ModelForm): class DocumentForm(forms.ModelForm):

View File

@@ -204,13 +204,20 @@ class DocumentViewTestMixin(object):
} }
) )
def _request_document_download_form_view(self): def _request_document_download_form_get_view(self):
return self.get( return self.get(
viewname='documents:document_download_form', kwargs={ viewname='documents:document_download_form', kwargs={
'pk': self.test_document.pk 'pk': self.test_document.pk
} }
) )
def _request_document_download_form_post_view(self):
return self.post(
viewname='documents:document_download_form', kwargs={
'pk': self.test_document.pk
}
)
def _request_document_download_view(self): def _request_document_download_view(self):
return self.get( return self.get(
viewname='documents:document_download', kwargs={ viewname='documents:document_download', kwargs={

View File

@@ -4,7 +4,6 @@ import time
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django_downloadview import assert_download_response
from rest_framework import status from rest_framework import status
from mayan.apps.rest_api.tests.base import BaseAPITestCase from mayan.apps.rest_api.tests.base import BaseAPITestCase
@@ -213,12 +212,10 @@ class DocumentAPIViewTestCase(
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
with self.test_document.open() as file_object: with self.test_document.open() as file_object:
assert_download_response( self.assert_download_response(
self, response, content=file_object.read(), response=response, content=file_object.read(),
basename=TEST_SMALL_DOCUMENT_FILENAME, filename=TEST_SMALL_DOCUMENT_FILENAME,
mime_type='{}; charset=utf-8'.format( mime_type=self.test_document.file_mimetype
self.test_document.file_mimetype
)
) )
def test_document_api_upload_view_no_permission(self): def test_document_api_upload_view_no_permission(self):
@@ -418,12 +415,10 @@ class DocumentVersionAPIViewTestCase(
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
with self.test_document.latest_version.open() as file_object: with self.test_document.latest_version.open() as file_object:
assert_download_response( self.assert_download_response(
self, response, content=file_object.read(), response=response, content=file_object.read(),
basename=force_text(self.test_document.latest_version), filename=force_text(self.test_document.latest_version),
mime_type='{}; charset=utf-8'.format( mime_type=self.test_document.file_mimetype
self.test_document.file_mimetype
)
) )
def test_document_version_api_download_preserve_extension_view(self): def test_document_version_api_download_preserve_extension_view(self):
@@ -440,13 +435,11 @@ class DocumentVersionAPIViewTestCase(
) )
with self.test_document.latest_version.open() as file_object: with self.test_document.latest_version.open() as file_object:
assert_download_response( self.assert_download_response(
self, response, content=file_object.read(), response=response, content=file_object.read(),
basename=self.test_document.latest_version.get_rendered_string( filename=self.test_document.latest_version.get_rendered_string(
preserve_extension=True preserve_extension=True
), mime_type='{}; charset=utf-8'.format( ), mime_type=self.test_document.file_mimetype
self.test_document.file_mimetype
)
) )
def test_document_version_api_list_view_no_permission(self): def test_document_version_api_list_view_no_permission(self):

View File

@@ -166,33 +166,40 @@ class DocumentViewTestCase(
Document.objects.first().document_type, document_type_2 Document.objects.first().document_type, document_type_2
) )
def test_document_download_form_view_no_permission(self): def test_document_download_form_get_view_no_permission(self):
response = self._request_document_download_form_view() response = self._request_document_download_form_get_view()
self.assertNotContains( self.assertEqual(response.status_code, 404)
response=response, text=self.test_document.label, status_code=200
)
def test_document_download_form_view_with_access(self): def test_document_download_form_get_view_with_access(self):
self.grant_access( self.grant_access(
obj=self.test_document, permission=permission_document_download obj=self.test_document, permission=permission_document_download
) )
response = self._request_document_download_form_view() response = self._request_document_download_form_get_view()
self.assertContains( self.assertContains(
response=response, text=self.test_document.label, status_code=200 response=response, text=self.test_document.label, status_code=200
) )
def test_document_download_form_post_view_no_permission(self):
response = self._request_document_download_form_post_view()
self.assertEqual(response.status_code, 404)
def test_document_download_form_post_view_with_access(self):
self.grant_access(
obj=self.test_document, permission=permission_document_download
)
response = self._request_document_download_form_post_view()
self.assertEqual(response.status_code, 302)
def test_document_download_view_no_permission(self): def test_document_download_view_no_permission(self):
response = self._request_document_download_view() response = self._request_document_download_view()
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 404)
def test_document_download_view_with_permission(self): def test_document_download_view_with_permission(self):
# Set the expected_content_types for # Set the expected_content_types for
# common.tests.mixins.ContentTypeCheckMixin # common.tests.mixins.ContentTypeCheckMixin
self.expected_content_types = ( self.expected_content_types = (
'{}; charset=utf-8'.format( self.test_document.file_mimetype,
self.test_document.file_mimetype
),
) )
self.grant_access( self.grant_access(
@@ -205,21 +212,19 @@ class DocumentViewTestCase(
with self.test_document.open() as file_object: with self.test_document.open() as file_object:
self.assert_download_response( self.assert_download_response(
response=response, content=file_object.read(), response=response, content=file_object.read(),
basename=TEST_SMALL_DOCUMENT_FILENAME, filename=TEST_SMALL_DOCUMENT_FILENAME,
mime_type=self.test_document.file_mimetype mime_type=self.test_document.file_mimetype
) )
def test_document_multiple_download_view_no_permission(self): def test_document_multiple_download_view_no_permission(self):
response = self._request_document_multiple_download_view() response = self._request_document_multiple_download_view()
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 404)
def test_document_multiple_download_view_with_permission(self): def test_document_multiple_download_view_with_permission(self):
# Set the expected_content_types for # Set the expected_content_types for
# common.tests.mixins.ContentTypeCheckMixin # common.tests.mixins.ContentTypeCheckMixin
self.expected_content_types = ( self.expected_content_types = (
'{}; charset=utf-8'.format( self.test_document.file_mimetype,
self.test_document.file_mimetype
),
) )
self.grant_access( self.grant_access(
obj=self.test_document, permission=permission_document_download obj=self.test_document, permission=permission_document_download
@@ -231,21 +236,19 @@ class DocumentViewTestCase(
with self.test_document.open() as file_object: with self.test_document.open() as file_object:
self.assert_download_response( self.assert_download_response(
response=response, content=file_object.read(), response=response, content=file_object.read(),
basename=TEST_SMALL_DOCUMENT_FILENAME, filename=TEST_SMALL_DOCUMENT_FILENAME,
mime_type=self.test_document.file_mimetype mime_type=self.test_document.file_mimetype
) )
def test_document_version_download_view_no_permission(self): def test_document_version_download_view_no_permission(self):
response = self._request_document_version_download() response = self._request_document_version_download()
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 404)
def test_document_version_download_view_with_permission(self): def test_document_version_download_view_with_permission(self):
# Set the expected_content_types for # Set the expected_content_types for
# common.tests.mixins.ContentTypeCheckMixin # common.tests.mixins.ContentTypeCheckMixin
self.expected_content_types = ( self.expected_content_types = (
'{}; charset=utf-8'.format( self.test_document.latest_version.mimetype,
self.test_document.latest_version.mimetype
),
) )
self.grant_access( self.grant_access(
@@ -258,19 +261,15 @@ class DocumentViewTestCase(
with self.test_document.open() as file_object: with self.test_document.open() as file_object:
self.assert_download_response( self.assert_download_response(
response=response, content=file_object.read(), response=response, content=file_object.read(),
basename=force_text(self.test_document.latest_version), filename=force_text(self.test_document.latest_version),
mime_type='{}; charset=utf-8'.format( mime_type=self.test_document.latest_version.mimetype
self.test_document.latest_version.mimetype
)
) )
def test_document_version_download_preserve_extension_view_with_permission(self): def test_document_version_download_preserve_extension_view_with_permission(self):
# Set the expected_content_types for # Set the expected_content_types for
# common.tests.mixins.ContentTypeCheckMixin # common.tests.mixins.ContentTypeCheckMixin
self.expected_content_types = ( self.expected_content_types = (
'{}; charset=utf-8'.format( self.test_document.latest_version.mimetype,
self.test_document.latest_version.mimetype
),
) )
self.grant_access( self.grant_access(
@@ -285,11 +284,9 @@ class DocumentViewTestCase(
with self.test_document.open() as file_object: with self.test_document.open() as file_object:
self.assert_download_response( self.assert_download_response(
response=response, content=file_object.read(), response=response, content=file_object.read(),
basename=self.test_document.latest_version.get_rendered_string( filename=self.test_document.latest_version.get_rendered_string(
preserve_extension=True preserve_extension=True
), mime_type='{}; charset=utf-8'.format( ), mime_type=self.test_document.latest_version.mimetype
self.test_document.latest_version.mimetype
)
) )
def test_document_update_page_count_view_no_permission(self): def test_document_update_page_count_view_no_permission(self):

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from actstream.models import Action from actstream.models import Action
from django_downloadview import assert_download_response
from ..events import ( from ..events import (
event_document_download, event_document_trashed, event_document_view event_document_download, event_document_trashed, event_document_view
@@ -45,11 +44,11 @@ class DocumentEventsTestCase(
def test_document_download_event_no_permission(self): def test_document_download_event_no_permission(self):
response = self._request_test_document_download_view() response = self._request_test_document_download_view()
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 404)
self.assertEqual(list(Action.objects.any(obj=self.test_document)), []) self.assertEqual(list(Action.objects.any(obj=self.test_document)), [])
def test_document_download_event_with_access(self): def test_document_download_event_with_access(self):
self.expected_content_types = ('image/png; charset=utf-8',) self.expected_content_types = ('image/png',)
self.grant_access( self.grant_access(
obj=self.test_document, permission=permission_document_download obj=self.test_document, permission=permission_document_download
@@ -59,8 +58,8 @@ class DocumentEventsTestCase(
# Download the file to close the file descriptor # Download the file to close the file descriptor
with self.test_document.open() as file_object: with self.test_document.open() as file_object:
assert_download_response( self.assert_download_response(
self, response, content=file_object.read(), response=response, content=file_object.read(),
mime_type=self.test_document.file_mimetype mime_type=self.test_document.file_mimetype
) )

View File

@@ -272,6 +272,11 @@ urlpatterns_document_versions = [
view=DocumentVersionDownloadView.as_view(), view=DocumentVersionDownloadView.as_view(),
name='document_version_download' name='document_version_download'
), ),
url(
regex=r'^documents/versions/multiple/download/$',
view=DocumentVersionDownloadView.as_view(),
name='document_multiple_version_download'
),
url( url(
regex=r'^documents/versions/(?P<pk>\d+)/revert/$', regex=r'^documents/versions/(?P<pk>\d+)/revert/$',
view=DocumentVersionRevertView.as_view(), view=DocumentVersionRevertView.as_view(),

View File

@@ -14,8 +14,7 @@ from ..events import event_document_view
from ..forms import DocumentVersionDownloadForm, DocumentVersionPreviewForm from ..forms import DocumentVersionDownloadForm, DocumentVersionPreviewForm
from ..models import Document, DocumentVersion from ..models import Document, DocumentVersion
from ..permissions import ( from ..permissions import (
permission_document_download, permission_document_version_revert, permission_document_version_revert, permission_document_version_view
permission_document_version_view
) )
from .document_views import DocumentDownloadFormView, DocumentDownloadView from .document_views import DocumentDownloadFormView, DocumentDownloadView
@@ -31,11 +30,11 @@ logger = logging.getLogger(__name__)
class DocumentVersionDownloadFormView(DocumentDownloadFormView): class DocumentVersionDownloadFormView(DocumentDownloadFormView):
form_class = DocumentVersionDownloadForm form_class = DocumentVersionDownloadForm
model = DocumentVersion model = DocumentVersion
multiple_download_view = None pk_url_kwarg = 'pk'
querystring_form_fields = ( querystring_form_fields = (
'compressed', 'zip_filename', 'preserve_extension' 'compressed', 'zip_filename', 'preserve_extension'
) )
single_download_view = 'documents:document_version_download' viewname = 'documents:document_multiple_version_download'
def get_extra_context(self): def get_extra_context(self):
result = super( result = super(
@@ -48,31 +47,12 @@ class DocumentVersionDownloadFormView(DocumentDownloadFormView):
return result return result
def get_document_queryset(self):
id_list = self.request.GET.get(
'id_list', self.request.POST.get('id_list', '')
)
if not id_list:
id_list = self.kwargs['pk']
return self.model.objects.filter(
pk__in=id_list.split(',')
)
class DocumentVersionDownloadView(DocumentDownloadView): class DocumentVersionDownloadView(DocumentDownloadView):
model = DocumentVersion model = DocumentVersion
object_permission = permission_document_download pk_url_kwarg = 'pk'
@staticmethod def get_item_filename(self, item):
def get_item_file(item):
return item.file
def get_encoding(self):
return self.get_object().encoding
def get_item_label(self, item):
preserve_extension = self.request.GET.get( preserve_extension = self.request.GET.get(
'preserve_extension', self.request.POST.get( 'preserve_extension', self.request.POST.get(
'preserve_extension', False 'preserve_extension', False
@@ -83,9 +63,6 @@ class DocumentVersionDownloadView(DocumentDownloadView):
return item.get_rendered_string(preserve_extension=preserve_extension) return item.get_rendered_string(preserve_extension=preserve_extension)
def get_mimetype(self):
return self.get_object().mimetype
class DocumentVersionListView(ExternalObjectMixin, SingleObjectListView): class DocumentVersionListView(ExternalObjectMixin, SingleObjectListView):
external_object_class = Document external_object_class = Document

View File

@@ -2,22 +2,23 @@ from __future__ import absolute_import, unicode_literals
import logging import logging
from furl import furl
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.utils.http import urlencode from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _, ungettext from django.utils.translation import ugettext_lazy as _, ungettext
from mayan.apps.acls.models import AccessControlList from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.compressed_files import ZipArchive from mayan.apps.common.compressed_files import ZipArchive
from mayan.apps.common.generics import ( from mayan.apps.common.generics import (
FormView, MultipleObjectConfirmActionView, MultipleObjectFormActionView, FormView, MultipleObjectConfirmActionView, MultipleObjectDownloadView,
SingleObjectDetailView, SingleObjectDownloadView, SingleObjectEditView, MultipleObjectFormActionView, SingleObjectDetailView,
SingleObjectListView SingleObjectEditView, SingleObjectListView
) )
from mayan.apps.converter.layers import layer_saved_transformations from mayan.apps.converter.layers import layer_saved_transformations
from mayan.apps.converter.permissions import ( from mayan.apps.converter.permissions import (
@@ -154,55 +155,36 @@ class DocumentDocumentTypeEditView(MultipleObjectFormActionView):
) )
class DocumentDownloadFormView(FormView): class DocumentDownloadFormView(MultipleObjectFormActionView):
form_class = DocumentDownloadForm form_class = DocumentDownloadForm
model = Document model = Document
multiple_download_view = 'documents:document_multiple_download' object_permission = permission_document_download
pk_url_kwarg = 'pk'
querystring_form_fields = ('compressed', 'zip_filename') querystring_form_fields = ('compressed', 'zip_filename')
single_download_view = 'documents:document_download' viewname = 'documents:document_multiple_download'
def form_valid(self, form): def form_valid(self, form):
querystring_dictionary = {} # Turn a queryset into a comma separated list of primary keys
id_list = ','.join(
[
force_text(pk) for pk in self.get_object_list().values_list('pk', flat=True)
]
)
# Construct URL with querystring to pass on to the next view
url = furl(
args={
'id_list': id_list
}, path=reverse(viewname=self.viewname)
)
# Pass the form field data as URL querystring to the next view
for field in self.querystring_form_fields: for field in self.querystring_form_fields:
data = form.cleaned_data[field] data = form.cleaned_data[field]
if data: if data:
querystring_dictionary[field] = data url.args[field] = data
querystring_dictionary.update( return HttpResponseRedirect(redirect_to=url.tostr())
{
'id_list': ','.join(
map(str, self.queryset.values_list('pk', flat=True))
)
}
)
querystring = urlencode(querystring_dictionary, doseq=True)
if self.queryset.count() > 1:
url = reverse(self.multiple_download_view)
else:
url = reverse(
viewname=self.single_download_view, kwargs={
'pk': self.queryset.first().pk
}
)
return HttpResponseRedirect(
redirect_to='{}?{}'.format(url, querystring)
)
def get_document_queryset(self):
id_list = self.request.GET.get(
'id_list', self.request.POST.get('id_list', '')
)
if not id_list:
id_list = self.kwargs['pk']
return self.model.objects.filter(
pk__in=id_list.split(',')
)
def get_extra_context(self): def get_extra_context(self):
subtemplates_list = [ subtemplates_list = [
@@ -210,7 +192,6 @@ class DocumentDownloadFormView(FormView):
'name': 'appearance/generic_list_items_subtemplate.html', 'name': 'appearance/generic_list_items_subtemplate.html',
'context': { 'context': {
'object_list': self.queryset, 'object_list': self.queryset,
'hide_link': True,
'hide_links': True, 'hide_links': True,
'hide_multi_item_actions': True, 'hide_multi_item_actions': True,
} }
@@ -230,21 +211,14 @@ class DocumentDownloadFormView(FormView):
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super(DocumentDownloadFormView, self).get_form_kwargs() kwargs = super(DocumentDownloadFormView, self).get_form_kwargs()
self.queryset = self.get_queryset() self.queryset = self.get_object_list()
kwargs.update({'queryset': self.queryset}) kwargs.update({'queryset': self.queryset})
return kwargs return kwargs
def get_queryset(self):
return AccessControlList.objects.restrict_queryset(
permission=permission_document_download,
queryset=self.get_document_queryset(), user=self.request.user
)
class DocumentDownloadView(MultipleObjectDownloadView):
class DocumentDownloadView(SingleObjectDownloadView):
model = Document model = Document
# Set to None to disable the .get_object call object_permission = permission_document_download
object_permission = None
@staticmethod @staticmethod
def commit_event(item, request): def commit_event(item, request):
@@ -259,39 +233,23 @@ class DocumentDownloadView(SingleObjectDownloadView):
target=item.document target=item.document
) )
@staticmethod def get_archive_filename(self):
def get_item_file(item): return self.request.GET.get(
return item.open()
def get_document_queryset(self):
id_list = self.request.GET.get(
'id_list', self.request.POST.get('id_list', '')
)
if not id_list:
id_list = self.kwargs['pk']
queryset = self.model.objects.filter(pk__in=id_list.split(','))
return AccessControlList.objects.restrict_queryset(
permission=permission_document_download, queryset=queryset,
user=self.request.user
)
def get_file(self):
queryset = self.get_document_queryset()
zip_filename = self.request.GET.get(
'zip_filename', DEFAULT_ZIP_FILENAME 'zip_filename', DEFAULT_ZIP_FILENAME
) )
def get_download_file_object(self):
queryset = self.get_object_list()
zip_filename = self.get_archive_filename()
if self.request.GET.get('compressed') == 'True' or queryset.count() > 1: if self.request.GET.get('compressed') == 'True' or queryset.count() > 1:
compressed_file = ZipArchive() compressed_file = ZipArchive()
compressed_file.create() compressed_file.create()
for item in queryset: for item in queryset:
with DocumentDownloadView.get_item_file(item=item) as file_object: with item.open() as file_object:
compressed_file.add_file( compressed_file.add_file(
file_object=file_object, file_object=file_object,
filename=self.get_item_label(item=item) filename=self.get_item_filename(item=item)
) )
DocumentDownloadView.commit_event( DocumentDownloadView.commit_event(
item=item, request=self.request item=item, request=self.request
@@ -299,24 +257,22 @@ class DocumentDownloadView(SingleObjectDownloadView):
compressed_file.close() compressed_file.close()
return DocumentDownloadView.VirtualFile( return compressed_file.as_file(zip_filename)
compressed_file.as_file(zip_filename), name=zip_filename
)
else: else:
item = queryset.first() item = queryset.first()
if item: DocumentDownloadView.commit_event(
DocumentDownloadView.commit_event( item=item, request=self.request
item=item, request=self.request
)
else:
raise PermissionDenied
return DocumentDownloadView.VirtualFile(
DocumentDownloadView.get_item_file(item=item),
name=self.get_item_label(item=item)
) )
return item.open()
def get_item_label(self, item): def get_download_filename(self):
queryset = self.get_object_list()
if self.request.GET.get('compressed') == 'True' or queryset.count() > 1:
return self.get_archive_filename()
else:
return self.get_item_filename(item=queryset.first())
def get_item_filename(self, item):
return item.label return item.label

View File

@@ -169,11 +169,11 @@ class OCRViewsTestCase(OCRViewTestMixin, GenericDocumentViewTestCase):
self.test_document.submit_for_ocr() self.test_document.submit_for_ocr()
response = self._request_document_ocr_download_view() response = self._request_document_ocr_download_view()
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 404)
def test_document_ocr_download_view_with_access(self): def test_document_ocr_download_view_with_access(self):
self.test_document.submit_for_ocr() self.test_document.submit_for_ocr()
self.expected_content_types = ('application/octet-stream; charset=utf-8',) self.expected_content_types = ('text/html; charset=utf-8',)
self.grant_access( self.grant_access(
obj=self.test_document, permission=permission_ocr_content_view obj=self.test_document, permission=permission_ocr_content_view

View File

@@ -211,10 +211,8 @@ class DocumentOCRDownloadView(SingleObjectDownloadView):
model = Document model = Document
object_permission = permission_ocr_content_view object_permission = permission_ocr_content_view
def get_file(self): def get_download_file_object(self):
file_object = DocumentOCRDownloadView.TextIteratorIO( return get_document_ocr_content(document=self.object)
iterator=get_document_ocr_content(document=self.get_object())
) def get_download_filename(self):
return DocumentOCRDownloadView.VirtualFile( return '{}-OCR'.format(self.object)
file=file_object, name='{}-OCR'.format(self.get_object())
)

View File

@@ -2,6 +2,7 @@
cssmin cssmin
django-autoadmin django-autoadmin
django-celery django-celery
django-downloadview
django-environ django-environ
django-suit django-suit
django-compressor django-compressor