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 BaseTransformationType metaclass in a way compatible with
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)
==================

View File

@@ -1,8 +1,14 @@
from __future__ import unicode_literals
import os
import types
from django.conf import settings
from django.http.response import StreamingHttpResponse
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:
dict_type = dict
@@ -22,3 +28,75 @@ except NameError:
FileNotFoundErrorException = IOError
else:
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 (
FormView as DjangoFormView, DetailView, TemplateView
)
from django.views.generic.base import View
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import (
CreateView, DeleteView, FormMixin, ModelFormMixin, UpdateView
)
from django.views.generic.list import ListView
from django_downloadview import (
TextIteratorIO, VirtualDownloadView, VirtualFile
)
from pure_pagination.mixins import PaginationMixin
from mayan.apps.acls.models import AccessControlList
@@ -36,11 +34,10 @@ from .literals import (
TEXT_SORT_ORDER_VARIABLE_NAME
)
from .mixins import (
DeleteExtraDataMixin, DynamicFormViewMixin, ExternalObjectMixin,
ExtraContextMixin, FormExtraKwargsMixin, MultipleObjectMixin,
ObjectActionMixin, ObjectNameMixin,
ObjectPermissionCheckMixin, RedirectionMixin, RestrictedQuerysetMixin,
ViewPermissionCheckMixin
DeleteExtraDataMixin, DownloadMixin, DynamicFormViewMixin,
ExternalObjectMixin, ExtraContextMixin, FormExtraKwargsMixin,
MultipleObjectMixin, ObjectActionMixin, ObjectNameMixin,
RedirectionMixin, RestrictedQuerysetMixin, ViewPermissionCheckMixin
)
from .settings import setting_paginate_by
@@ -491,7 +488,9 @@ class MultipleObjectConfirmActionView(
class SimpleView(ViewPermissionCheckMixin, ExtraContextMixin, TemplateView):
pass
"""
Basic template view class with permission check and extra context
"""
class SingleObjectCreateView(
@@ -657,9 +656,52 @@ class SingleObjectDetailView(
return super(SingleObjectDetailView, self).get_queryset()
class SingleObjectDownloadView(ViewPermissionCheckMixin, ObjectPermissionCheckMixin, VirtualDownloadView, SingleObjectMixin):
TextIteratorIO = TextIteratorIO
VirtualFile = VirtualFile
class BaseDownloadView(DownloadMixin, ViewPermissionCheckMixin, View):
def get(self, request, *args, **kwargs):
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(

View File

@@ -13,6 +13,7 @@ from mayan.apps.acls.classes import ModelPermission
from mayan.apps.acls.models import AccessControlList
from mayan.apps.permissions import Permission
from .compat import FileResponse
from .exceptions import ActionError
from .forms import DynamicForm
from .literals import PK_LIST_SEPARATOR
@@ -56,6 +57,29 @@ class DeleteExtraDataMixin(object):
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):
form_class = DynamicForm
@@ -345,26 +369,6 @@ class ObjectNameMixin(object):
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):
action_cancel_redirect = None
next_url = None

View File

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

View File

@@ -18,12 +18,16 @@ from django.http import HttpResponse
from django.template import Context, Template
from django.test.utils import ContextList
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 mayan.apps.acls.classes import ModelPermission
from mayan.apps.storage.settings import setting_temporary_directory
from ..compat import FileResponse
from .literals import (
TEST_SERVER_HOST, TEST_SERVER_SCHEME, TEST_VIEW_NAME, TEST_VIEW_URL
)
@@ -141,6 +145,37 @@ class ContentTypeCheckTestCaseMixin(object):
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):
def setUp(self):
super(EnvironmentTestCaseMixin, self).setUp()

View File

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

View File

@@ -3,7 +3,6 @@ from __future__ import absolute_import, unicode_literals
import logging
from django.contrib import messages
from django.core.files.base import ContentFile
from django.template import RequestContext
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
@@ -56,10 +55,11 @@ class KeyDownloadView(SingleObjectDownloadView):
model = Key
object_permission = permission_key_download
def get_file(self):
key = self.get_object()
def get_download_file_object(self):
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):

View File

@@ -114,12 +114,10 @@ class DocumentContentViewsTestCase(
def test_document_parsing_download_view_no_permission(self):
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):
self.expected_content_types = (
'application/octet-stream; charset=utf-8',
)
self.expected_content_types = ('text/html; charset=utf-8',)
self.grant_access(
obj=self.test_document, permission=permission_content_view
)

View File

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

View File

@@ -1,7 +1,5 @@
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.tests.mixins import KeyTestMixin
from mayan.apps.documents.models import DocumentVersion
@@ -287,7 +285,7 @@ class DetachedSignaturesViewTestCase(
self._create_test_detached_signature()
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):
self.test_document_path = TEST_SMALL_DOCUMENT_PATH
@@ -300,13 +298,13 @@ class DetachedSignaturesViewTestCase(
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()
with self.test_signature.signature_file as file_object:
assert_download_response(
self, response=response, content=file_object.read(),
self.assert_download_response(
response=response, content=file_object.read(),
)
def test_signature_upload_view_no_permission(self):

View File

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

View File

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

View File

@@ -45,7 +45,9 @@ class DocumentDownloadForm(forms.Form):
super(DocumentDownloadForm, self).__init__(*args, **kwargs)
if self.queryset.count() > 1:
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):

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(
viewname='documents:document_download_form', kwargs={
'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):
return self.get(
viewname='documents:document_download', kwargs={

View File

@@ -4,7 +4,6 @@ import time
from django.utils.encoding import force_text
from django_downloadview import assert_download_response
from rest_framework import status
from mayan.apps.rest_api.tests.base import BaseAPITestCase
@@ -213,12 +212,10 @@ class DocumentAPIViewTestCase(
self.assertEqual(response.status_code, status.HTTP_200_OK)
with self.test_document.open() as file_object:
assert_download_response(
self, response, content=file_object.read(),
basename=TEST_SMALL_DOCUMENT_FILENAME,
mime_type='{}; charset=utf-8'.format(
self.test_document.file_mimetype
)
self.assert_download_response(
response=response, content=file_object.read(),
filename=TEST_SMALL_DOCUMENT_FILENAME,
mime_type=self.test_document.file_mimetype
)
def test_document_api_upload_view_no_permission(self):
@@ -418,12 +415,10 @@ class DocumentVersionAPIViewTestCase(
self.assertEqual(response.status_code, status.HTTP_200_OK)
with self.test_document.latest_version.open() as file_object:
assert_download_response(
self, response, content=file_object.read(),
basename=force_text(self.test_document.latest_version),
mime_type='{}; charset=utf-8'.format(
self.test_document.file_mimetype
)
self.assert_download_response(
response=response, content=file_object.read(),
filename=force_text(self.test_document.latest_version),
mime_type=self.test_document.file_mimetype
)
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:
assert_download_response(
self, response, content=file_object.read(),
basename=self.test_document.latest_version.get_rendered_string(
self.assert_download_response(
response=response, content=file_object.read(),
filename=self.test_document.latest_version.get_rendered_string(
preserve_extension=True
), mime_type='{}; charset=utf-8'.format(
self.test_document.file_mimetype
)
), mime_type=self.test_document.file_mimetype
)
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
)
def test_document_download_form_view_no_permission(self):
response = self._request_document_download_form_view()
self.assertNotContains(
response=response, text=self.test_document.label, status_code=200
)
def test_document_download_form_get_view_no_permission(self):
response = self._request_document_download_form_get_view()
self.assertEqual(response.status_code, 404)
def test_document_download_form_view_with_access(self):
def test_document_download_form_get_view_with_access(self):
self.grant_access(
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(
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):
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):
# Set the expected_content_types for
# common.tests.mixins.ContentTypeCheckMixin
self.expected_content_types = (
'{}; charset=utf-8'.format(
self.test_document.file_mimetype
),
self.test_document.file_mimetype,
)
self.grant_access(
@@ -205,21 +212,19 @@ class DocumentViewTestCase(
with self.test_document.open() as file_object:
self.assert_download_response(
response=response, content=file_object.read(),
basename=TEST_SMALL_DOCUMENT_FILENAME,
filename=TEST_SMALL_DOCUMENT_FILENAME,
mime_type=self.test_document.file_mimetype
)
def test_document_multiple_download_view_no_permission(self):
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):
# Set the expected_content_types for
# common.tests.mixins.ContentTypeCheckMixin
self.expected_content_types = (
'{}; charset=utf-8'.format(
self.test_document.file_mimetype
),
self.test_document.file_mimetype,
)
self.grant_access(
obj=self.test_document, permission=permission_document_download
@@ -231,21 +236,19 @@ class DocumentViewTestCase(
with self.test_document.open() as file_object:
self.assert_download_response(
response=response, content=file_object.read(),
basename=TEST_SMALL_DOCUMENT_FILENAME,
filename=TEST_SMALL_DOCUMENT_FILENAME,
mime_type=self.test_document.file_mimetype
)
def test_document_version_download_view_no_permission(self):
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):
# Set the expected_content_types for
# common.tests.mixins.ContentTypeCheckMixin
self.expected_content_types = (
'{}; charset=utf-8'.format(
self.test_document.latest_version.mimetype
),
self.test_document.latest_version.mimetype,
)
self.grant_access(
@@ -258,19 +261,15 @@ class DocumentViewTestCase(
with self.test_document.open() as file_object:
self.assert_download_response(
response=response, content=file_object.read(),
basename=force_text(self.test_document.latest_version),
mime_type='{}; charset=utf-8'.format(
self.test_document.latest_version.mimetype
)
filename=force_text(self.test_document.latest_version),
mime_type=self.test_document.latest_version.mimetype
)
def test_document_version_download_preserve_extension_view_with_permission(self):
# Set the expected_content_types for
# common.tests.mixins.ContentTypeCheckMixin
self.expected_content_types = (
'{}; charset=utf-8'.format(
self.test_document.latest_version.mimetype
),
self.test_document.latest_version.mimetype,
)
self.grant_access(
@@ -285,11 +284,9 @@ class DocumentViewTestCase(
with self.test_document.open() as file_object:
self.assert_download_response(
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
), mime_type='{}; charset=utf-8'.format(
self.test_document.latest_version.mimetype
)
), mime_type=self.test_document.latest_version.mimetype
)
def test_document_update_page_count_view_no_permission(self):

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
from actstream.models import Action
from django_downloadview import assert_download_response
from ..events import (
event_document_download, event_document_trashed, event_document_view
@@ -45,11 +44,11 @@ class DocumentEventsTestCase(
def test_document_download_event_no_permission(self):
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)), [])
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(
obj=self.test_document, permission=permission_document_download
@@ -59,8 +58,8 @@ class DocumentEventsTestCase(
# Download the file to close the file descriptor
with self.test_document.open() as file_object:
assert_download_response(
self, response, content=file_object.read(),
self.assert_download_response(
response=response, content=file_object.read(),
mime_type=self.test_document.file_mimetype
)

View File

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

View File

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

View File

@@ -2,22 +2,23 @@ from __future__ import absolute_import, unicode_literals
import logging
from furl import furl
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
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 mayan.apps.acls.models import AccessControlList
from mayan.apps.common.compressed_files import ZipArchive
from mayan.apps.common.generics import (
FormView, MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectDetailView, SingleObjectDownloadView, SingleObjectEditView,
SingleObjectListView
FormView, MultipleObjectConfirmActionView, MultipleObjectDownloadView,
MultipleObjectFormActionView, SingleObjectDetailView,
SingleObjectEditView, SingleObjectListView
)
from mayan.apps.converter.layers import layer_saved_transformations
from mayan.apps.converter.permissions import (
@@ -154,55 +155,36 @@ class DocumentDocumentTypeEditView(MultipleObjectFormActionView):
)
class DocumentDownloadFormView(FormView):
class DocumentDownloadFormView(MultipleObjectFormActionView):
form_class = DocumentDownloadForm
model = Document
multiple_download_view = 'documents:document_multiple_download'
object_permission = permission_document_download
pk_url_kwarg = 'pk'
querystring_form_fields = ('compressed', 'zip_filename')
single_download_view = 'documents:document_download'
viewname = 'documents:document_multiple_download'
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:
data = form.cleaned_data[field]
if data:
querystring_dictionary[field] = data
url.args[field] = data
querystring_dictionary.update(
{
'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(',')
)
return HttpResponseRedirect(redirect_to=url.tostr())
def get_extra_context(self):
subtemplates_list = [
@@ -210,7 +192,6 @@ class DocumentDownloadFormView(FormView):
'name': 'appearance/generic_list_items_subtemplate.html',
'context': {
'object_list': self.queryset,
'hide_link': True,
'hide_links': True,
'hide_multi_item_actions': True,
}
@@ -230,21 +211,14 @@ class DocumentDownloadFormView(FormView):
def get_form_kwargs(self):
kwargs = super(DocumentDownloadFormView, self).get_form_kwargs()
self.queryset = self.get_queryset()
self.queryset = self.get_object_list()
kwargs.update({'queryset': self.queryset})
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(SingleObjectDownloadView):
class DocumentDownloadView(MultipleObjectDownloadView):
model = Document
# Set to None to disable the .get_object call
object_permission = None
object_permission = permission_document_download
@staticmethod
def commit_event(item, request):
@@ -259,39 +233,23 @@ class DocumentDownloadView(SingleObjectDownloadView):
target=item.document
)
@staticmethod
def get_item_file(item):
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(
def get_archive_filename(self):
return self.request.GET.get(
'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:
compressed_file = ZipArchive()
compressed_file.create()
for item in queryset:
with DocumentDownloadView.get_item_file(item=item) as file_object:
with item.open() as file_object:
compressed_file.add_file(
file_object=file_object,
filename=self.get_item_label(item=item)
filename=self.get_item_filename(item=item)
)
DocumentDownloadView.commit_event(
item=item, request=self.request
@@ -299,24 +257,22 @@ class DocumentDownloadView(SingleObjectDownloadView):
compressed_file.close()
return DocumentDownloadView.VirtualFile(
compressed_file.as_file(zip_filename), name=zip_filename
)
return compressed_file.as_file(zip_filename)
else:
item = queryset.first()
if item:
DocumentDownloadView.commit_event(
item=item, request=self.request
)
return item.open()
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:
raise PermissionDenied
return self.get_item_filename(item=queryset.first())
return DocumentDownloadView.VirtualFile(
DocumentDownloadView.get_item_file(item=item),
name=self.get_item_label(item=item)
)
def get_item_label(self, item):
def get_item_filename(self, item):
return item.label

View File

@@ -169,11 +169,11 @@ class OCRViewsTestCase(OCRViewTestMixin, GenericDocumentViewTestCase):
self.test_document.submit_for_ocr()
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):
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(
obj=self.test_document, permission=permission_ocr_content_view

View File

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

View File

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