mirror of
https://github.com/jazzband/django-downloadview.git
synced 2026-03-16 22:40:25 +00:00
Refreshed development environment. Added tests around Python 3.4 and Django 1.8. Fixed content proxied by HTTPFile. Closes #97, closes #98, closes #99.
This commit is contained in:
commit
211fd5461b
32 changed files with 799 additions and 662 deletions
17
.travis.yml
17
.travis.yml
|
|
@ -1,11 +1,20 @@
|
|||
language: python
|
||||
env:
|
||||
- TOXENV=py27
|
||||
- TOXENV=py33
|
||||
- TOXENV=py27-django15
|
||||
- TOXENV=py27-django16
|
||||
- TOXENV=py27-django17
|
||||
- TOXENV=py27-django18
|
||||
- TOXENV=py33-django15
|
||||
- TOXENV=py33-django16
|
||||
- TOXENV=py33-django17
|
||||
- TOXENV=py33-django18
|
||||
- TOXENV=py34-django15
|
||||
- TOXENV=py34-django16
|
||||
- TOXENV=py34-django17
|
||||
- TOXENV=py34-django18
|
||||
- TOXENV=flake8
|
||||
- TOXENV=sphinx
|
||||
- TOXENV=readme
|
||||
install:
|
||||
- pip install tox
|
||||
script:
|
||||
- tox
|
||||
- make test
|
||||
|
|
|
|||
12
CHANGELOG
12
CHANGELOG
|
|
@ -8,7 +8,17 @@ future releases, check `milestones`_ and :doc:`/about/vision`.
|
|||
1.7 (unreleased)
|
||||
----------------
|
||||
|
||||
- Nothing changed yet.
|
||||
- Bugfix #97 - ``HTTPFile`` proxies bytes as ``BytesIteratorIO`` (was undecoded
|
||||
urllib3 file object). ``StringIteratorIO`` has been split into
|
||||
``TextIteratorIO`` and ``BytesIteratorIO``. ``StringIteratorIO`` is
|
||||
deprecated but kept for backward compatibility as an alias for
|
||||
``TextIteratorIO``.
|
||||
|
||||
- Feature #99 - Tox runs project's tests with Python 2.7, 3.3 and 3.4, and with
|
||||
Django 1.5 to 1.8.
|
||||
|
||||
- Refactoring #98 - Refreshed development environment: packaging, Tox and
|
||||
Sphinx.
|
||||
|
||||
|
||||
1.6 (2014-03-03)
|
||||
|
|
|
|||
67
INSTALL
67
INSTALL
|
|
@ -7,28 +7,70 @@ Install
|
|||
If you want to install a development environment, please see
|
||||
:doc:`/contributing`.
|
||||
|
||||
System requirements:
|
||||
|
||||
* Python version 2.7
|
||||
************
|
||||
Requirements
|
||||
************
|
||||
|
||||
Install the package with your favorite Python installer. As an example, with
|
||||
pip:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
pip install django-downloadview
|
||||
`django-downloadview` has been tested with Python version 2.7, 3.3 and 3.4.
|
||||
|
||||
Installing `django-downloadview` will automatically trigger the installation of
|
||||
the following requirements:
|
||||
|
||||
.. literalinclude:: /../setup.py
|
||||
:language: python
|
||||
:lines: 39
|
||||
:start-after: BEGIN requirements
|
||||
:end-before: END requirements
|
||||
|
||||
.. note::
|
||||
|
||||
Since version 1.1, django-downloadview requires Django>=1.5, which provides
|
||||
:py:class:`~django.http.StreamingHttpResponse`.
|
||||
************
|
||||
As a library
|
||||
************
|
||||
|
||||
In most cases, you will use `django-downloadview` as a dependency of another
|
||||
project. In such a case, you should add `django-downloadview` in your main
|
||||
project's requirements. Typically in :file:`setup.py`:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
install_requires=[
|
||||
'django-downloadview',
|
||||
#...
|
||||
]
|
||||
# ...
|
||||
)
|
||||
|
||||
Then when you install your main project with your favorite package manager
|
||||
(like `pip`_), `django-downloadview` and its recursive dependencies will
|
||||
automatically be installed.
|
||||
|
||||
|
||||
**********
|
||||
Standalone
|
||||
**********
|
||||
|
||||
You can install `django-downloadview` with your favorite Python package
|
||||
manager. As an example with `pip`_:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
pip install django-downloadview
|
||||
|
||||
|
||||
*****
|
||||
Check
|
||||
*****
|
||||
|
||||
Check `django-downloadview` has been installed:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
python -c "import django_downloadview;print(django_downloadview.__version__)"
|
||||
|
||||
You should get installed `django-downloadview`'s version.
|
||||
|
||||
|
||||
.. rubric:: Notes & references
|
||||
|
|
@ -41,5 +83,6 @@ the following requirements:
|
|||
|
||||
.. target-notes::
|
||||
|
||||
.. _`pip`: https://pip.pypa.io/
|
||||
.. _`django-downloadview's bugtracker`:
|
||||
https://github.com/benoitbryon/django-downloadview/issues
|
||||
|
|
|
|||
53
Makefile
53
Makefile
|
|
@ -9,39 +9,30 @@ PIP = pip
|
|||
TOX = tox
|
||||
|
||||
|
||||
.PHONY: all help develop clean distclean maintainer-clean test documentation sphinx readme demo runserver release
|
||||
|
||||
|
||||
# Default target. Does nothing.
|
||||
all:
|
||||
@echo "Reference card for usual actions in development environment."
|
||||
@echo "Nothing to do by default."
|
||||
@echo "Try 'make help'."
|
||||
|
||||
|
||||
#: help - Display callable targets.
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Reference card for usual actions in development environment."
|
||||
@echo "Here are available targets:"
|
||||
@egrep -o "^#: (.+)" [Mm]akefile | sed 's/#: /* /'
|
||||
|
||||
|
||||
#: develop - Install minimal development utilities such as tox.
|
||||
#: develop - Install minimal development utilities.
|
||||
.PHONY: develop
|
||||
develop:
|
||||
mkdir -p var
|
||||
$(PIP) install tox
|
||||
$(PIP) install -e ./
|
||||
$(PIP) install -e ./demo/
|
||||
$(PIP) install -e .
|
||||
|
||||
|
||||
#: clean - Basic cleanup, mostly temporary files.
|
||||
.PHONY: clean
|
||||
clean:
|
||||
find . -name "*.pyc" -delete
|
||||
find . -name '*.pyo' -delete
|
||||
find . -name "__pycache__" -delete
|
||||
find . -name ".noseids" -delete
|
||||
|
||||
|
||||
#: distclean - Remove local builds, such as *.egg-info.
|
||||
.PHONY: distclean
|
||||
distclean: clean
|
||||
rm -rf *.egg
|
||||
rm -rf *.egg-info
|
||||
|
|
@ -49,35 +40,42 @@ distclean: clean
|
|||
|
||||
|
||||
#: maintainer-clean - Remove almost everything that can be re-generated.
|
||||
.PHONY: maintainer-clean
|
||||
maintainer-clean: distclean
|
||||
rm -rf bin/
|
||||
rm -rf lib/
|
||||
rm -rf build/
|
||||
rm -rf dist/
|
||||
rm -rf .tox/
|
||||
|
||||
|
||||
#: test - Run full test suite.
|
||||
#: test - Run test suites.
|
||||
.PHONY: test
|
||||
test:
|
||||
mkdir -p var
|
||||
$(PIP) install -e .[test]
|
||||
$(TOX)
|
||||
|
||||
|
||||
#: sphinx - Build Sphinx documentation.
|
||||
#: documentation - Build documentation (Sphinx, README, ...)
|
||||
.PHONY: documentation
|
||||
documentation: sphinx readme
|
||||
|
||||
|
||||
#: sphinx - Build Sphinx documentation (docs).
|
||||
.PHONY: sphinx
|
||||
sphinx:
|
||||
$(TOX) -e sphinx
|
||||
|
||||
|
||||
#: readme - Build standalone documentation files (README, CONTRIBUTING...).
|
||||
.PHONY: readme
|
||||
readme:
|
||||
$(TOX) -e readme
|
||||
|
||||
|
||||
#: documentation - Build full documentation.
|
||||
documentation: sphinx readme
|
||||
|
||||
|
||||
demo: develop
|
||||
demo syncdb --noinput
|
||||
#: demo - Setup demo project.
|
||||
.PHONY: demo
|
||||
demo:
|
||||
demo migrate --noinput
|
||||
# Install fixtures.
|
||||
mkdir -p var/media
|
||||
cp -r demo/demoproject/fixtures var/media/object
|
||||
|
|
@ -86,10 +84,13 @@ demo: develop
|
|||
demo loaddata demo.json
|
||||
|
||||
|
||||
#: runserver - Run demo server.
|
||||
.PHONY: runserver
|
||||
runserver: demo
|
||||
demo runserver
|
||||
|
||||
|
||||
#: release - Tag and push to PyPI.
|
||||
.PHONY: release
|
||||
release:
|
||||
$(TOX) -e release
|
||||
|
|
|
|||
15
README.rst
15
README.rst
|
|
@ -2,24 +2,24 @@
|
|||
django-downloadview
|
||||
###################
|
||||
|
||||
``django-downloadview`` makes it easy to serve files with Django:
|
||||
`django-downloadview` makes it easy to serve files with `Django`_:
|
||||
|
||||
* you manage files with Django (permissions, search, generation, ...);
|
||||
* you manage files with Django (permissions, filters, generation, ...);
|
||||
|
||||
* files are stored somewhere or generated somehow (local filesystem, remote
|
||||
storage, memory...);
|
||||
|
||||
* ``django-downloadview`` helps you stream the files with very little code;
|
||||
* `django-downloadview` helps you stream the files with very little code;
|
||||
|
||||
* ``django-downloadview`` helps you improve performances with reverse proxies,
|
||||
via mechanisms such as Nginx's X-Accel.
|
||||
* `django-downloadview` helps you improve performances with reverse proxies,
|
||||
via mechanisms such as Nginx's X-Accel or Apache's X-Sendfile.
|
||||
|
||||
|
||||
*******
|
||||
Example
|
||||
*******
|
||||
|
||||
Let's serve a file stored in a FileField of some model:
|
||||
Let's serve a file stored in a file field of some model:
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
|
@ -45,3 +45,6 @@ Resources
|
|||
* Bugtracker: https://github.com/benoitbryon/django-downloadview/issues
|
||||
* Continuous integration: https://travis-ci.org/benoitbryon/django-downloadview
|
||||
* Roadmap: https://github.com/benoitbryon/django-downloadview/milestones
|
||||
|
||||
|
||||
.. _`Django`: https://djangoproject.com
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ Execute:
|
|||
make runserver
|
||||
|
||||
It installs and runs the demo server on localhost, port 8000. So have a look
|
||||
at http://localhost:8000/
|
||||
at ``http://localhost:8000/``.
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Django settings for Django-DownloadView demo project."""
|
||||
from os.path import abspath, dirname, join
|
||||
"""Django settings for django-downloadview demo project."""
|
||||
import os
|
||||
|
||||
|
||||
# Configure some relative directories.
|
||||
demoproject_dir = dirname(abspath(__file__))
|
||||
demo_dir = dirname(demoproject_dir)
|
||||
root_dir = dirname(demo_dir)
|
||||
data_dir = join(root_dir, 'var')
|
||||
cfg_dir = join(root_dir, 'etc')
|
||||
demoproject_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
demo_dir = os.path.dirname(demoproject_dir)
|
||||
root_dir = os.path.dirname(demo_dir)
|
||||
data_dir = os.path.join(root_dir, 'var')
|
||||
cfg_dir = os.path.join(root_dir, 'etc')
|
||||
|
||||
|
||||
# Mandatory settings.
|
||||
|
|
@ -20,7 +20,7 @@ WSGI_APPLICATION = 'demoproject.wsgi.application'
|
|||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': join(data_dir, 'db.sqlite'),
|
||||
'NAME': os.path.join(data_dir, 'db.sqlite'),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,21 +29,14 @@ DATABASES = {
|
|||
SECRET_KEY = "This is a secret made public on project's repository."
|
||||
|
||||
# Media and static files.
|
||||
MEDIA_ROOT = join(data_dir, 'media')
|
||||
MEDIA_ROOT = os.path.join(data_dir, 'media')
|
||||
MEDIA_URL = '/media/'
|
||||
STATIC_ROOT = join(data_dir, 'static')
|
||||
STATIC_ROOT = os.path.join(data_dir, 'static')
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
|
||||
# Applications.
|
||||
INSTALLED_APPS = (
|
||||
# Standard Django applications.
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
# The actual django-downloadview demo.
|
||||
'demoproject',
|
||||
'demoproject.object', # Demo around ObjectDownloadView
|
||||
|
|
@ -54,13 +47,19 @@ INSTALLED_APPS = (
|
|||
'demoproject.nginx', # Sample optimizations for Nginx X-Accel.
|
||||
'demoproject.apache', # Sample optimizations for Apache X-Sendfile.
|
||||
'demoproject.lighttpd', # Sample optimizations for Lighttpd X-Sendfile.
|
||||
# For test purposes. The demo project is part of django-downloadview
|
||||
# test suite.
|
||||
# Standard Django applications.
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
# Stuff that must be at the end.
|
||||
'django_nose',
|
||||
)
|
||||
|
||||
|
||||
# Middlewares.
|
||||
# BEGIN middlewares
|
||||
MIDDLEWARE_CLASSES = [
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
|
|
@ -69,19 +68,27 @@ MIDDLEWARE_CLASSES = [
|
|||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django_downloadview.SmartDownloadMiddleware'
|
||||
]
|
||||
# END middlewares
|
||||
|
||||
|
||||
# Specific configuration for django_downloadview.SmartDownloadMiddleware.
|
||||
# BEGIN backend
|
||||
DOWNLOADVIEW_BACKEND = 'django_downloadview.nginx.XAccelRedirectMiddleware'
|
||||
# END backend
|
||||
"""Could also be:
|
||||
DOWNLOADVIEW_BACKEND = 'django_downloadview.apache.XSendfileMiddleware'
|
||||
DOWNLOADVIEW_BACKEND = 'django_downloadview.lighttpd.XSendfileMiddleware'
|
||||
"""
|
||||
|
||||
# BEGIN rules
|
||||
DOWNLOADVIEW_RULES = [
|
||||
{
|
||||
'source_url': '/media/nginx/',
|
||||
'destination_url': '/nginx-optimized-by-middleware/',
|
||||
},
|
||||
]
|
||||
# END rules
|
||||
DOWNLOADVIEW_RULES += [
|
||||
{
|
||||
'source_url': '/media/apache/',
|
||||
'destination_dir': '/apache-optimized-by-middleware/',
|
||||
|
|
@ -107,7 +114,10 @@ DOWNLOADVIEW_RULES = [
|
|||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
nose_cfg_dir = join(cfg_dir, 'nose')
|
||||
NOSE_ARGS = ['--config={etc}/base.cfg'.format(etc=nose_cfg_dir),
|
||||
'--config={etc}/{package}.cfg'.format(etc=nose_cfg_dir,
|
||||
package=__package__)]
|
||||
NOSE_ARGS = [
|
||||
'--verbosity=2',
|
||||
'--no-path-adjustment',
|
||||
'--nocapture',
|
||||
'--all-modules',
|
||||
'--with-coverage',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from django.core.files.base import ContentFile
|
|||
|
||||
from django_downloadview import VirtualDownloadView
|
||||
from django_downloadview import VirtualFile
|
||||
from django_downloadview import StringIteratorIO
|
||||
from django_downloadview import TextIteratorIO
|
||||
|
||||
|
||||
class TextDownloadView(VirtualDownloadView):
|
||||
|
|
@ -15,7 +15,7 @@ class TextDownloadView(VirtualDownloadView):
|
|||
|
||||
class StringIODownloadView(VirtualDownloadView):
|
||||
def get_file(self):
|
||||
"""Return wrapper on ``StringIO`` object."""
|
||||
"""Return wrapper on ``six.StringIO`` object."""
|
||||
file_obj = StringIO(u"Hello world!\n")
|
||||
return VirtualFile(file_obj, name='hello-world.txt')
|
||||
|
||||
|
|
@ -29,5 +29,5 @@ def generate_hello():
|
|||
class GeneratedDownloadView(VirtualDownloadView):
|
||||
def get_file(self):
|
||||
"""Return wrapper on ``StringIteratorIO`` object."""
|
||||
file_obj = StringIteratorIO(generate_hello())
|
||||
file_obj = TextIteratorIO(generate_hello())
|
||||
return VirtualFile(file_obj, name='hello-world.txt')
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ CLASSIFIERS = ['Development Status :: 4 - Beta',
|
|||
'Framework :: Django']
|
||||
KEYWORDS = []
|
||||
PACKAGES = ['demoproject']
|
||||
REQUIREMENTS = ['django-downloadview', 'django-nose']
|
||||
REQUIREMENTS = [
|
||||
'django-downloadview',
|
||||
'django-nose']
|
||||
ENTRY_POINTS = {
|
||||
'console_scripts': ['demo = demoproject.manage:main']
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Declaration of API shortcuts."""
|
||||
from django_downloadview.io import StringIteratorIO # NoQA
|
||||
from django_downloadview.io import (BytesIteratorIO, # NoQA
|
||||
TextIteratorIO)
|
||||
from django_downloadview.files import (StorageFile, # NoQA
|
||||
VirtualFile,
|
||||
HTTPFile)
|
||||
|
|
@ -20,3 +21,7 @@ from django_downloadview.shortcuts import sendfile # NoQA
|
|||
from django_downloadview.test import (assert_download_response, # NoQA
|
||||
setup_view,
|
||||
temporary_media_root)
|
||||
|
||||
|
||||
# Backward compatibility.
|
||||
StringIteratorIO = TextIteratorIO
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ from django.utils.encoding import force_bytes
|
|||
|
||||
import requests
|
||||
|
||||
from django_downloadview.io import BytesIteratorIO
|
||||
|
||||
|
||||
class StorageFile(File):
|
||||
"""A file in a Django storage.
|
||||
|
|
@ -239,7 +241,12 @@ class HTTPFile(File):
|
|||
|
||||
@property
|
||||
def file(self):
|
||||
return self.request.raw
|
||||
try:
|
||||
return self._file
|
||||
except AttributeError:
|
||||
content = self.request.iter_content(decode_unicode=False)
|
||||
self._file = BytesIteratorIO(content)
|
||||
return self._file
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@
|
|||
from __future__ import absolute_import
|
||||
import io
|
||||
|
||||
from django.utils.encoding import force_text, force_bytes
|
||||
|
||||
class StringIteratorIO(io.TextIOBase):
|
||||
"""A dynamically generated StringIO-like object.
|
||||
|
||||
class TextIteratorIO(io.TextIOBase):
|
||||
"""A dynamically generated TextIO-like object.
|
||||
|
||||
Original code by Matt Joiner <anacrolix@gmail.com> from:
|
||||
|
||||
|
|
@ -14,8 +16,11 @@ class StringIteratorIO(io.TextIOBase):
|
|||
|
||||
"""
|
||||
def __init__(self, iterator):
|
||||
#: Iterator/generator for content.
|
||||
self._iter = iterator
|
||||
self._left = ''
|
||||
|
||||
#: Internal buffer.
|
||||
self._left = u''
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
|
@ -26,11 +31,15 @@ class StringIteratorIO(io.TextIOBase):
|
|||
self._left = next(self._iter)
|
||||
except StopIteration:
|
||||
break
|
||||
else:
|
||||
# Make sure we handle text.
|
||||
self._left = force_text(self._left)
|
||||
ret = self._left[:n]
|
||||
self._left = self._left[len(ret):]
|
||||
return ret
|
||||
|
||||
def read(self, n=None):
|
||||
"""Return content up to ``n`` length."""
|
||||
l = []
|
||||
if n is None or n < 0:
|
||||
while True:
|
||||
|
|
@ -45,21 +54,89 @@ class StringIteratorIO(io.TextIOBase):
|
|||
break
|
||||
n -= len(m)
|
||||
l.append(m)
|
||||
return ''.join(l)
|
||||
return u''.join(l)
|
||||
|
||||
def readline(self):
|
||||
l = []
|
||||
while True:
|
||||
i = self._left.find('\n')
|
||||
i = self._left.find(u'\n')
|
||||
if i == -1:
|
||||
l.append(self._left)
|
||||
try:
|
||||
self._left = next(self._iter)
|
||||
except StopIteration:
|
||||
self._left = ''
|
||||
self._left = u''
|
||||
break
|
||||
else:
|
||||
l.append(self._left[:i + 1])
|
||||
self._left = self._left[i + 1:]
|
||||
break
|
||||
return ''.join(l)
|
||||
return u''.join(l)
|
||||
|
||||
|
||||
class BytesIteratorIO(io.BytesIO):
|
||||
"""A dynamically generated BytesIO-like object.
|
||||
|
||||
Original code by Matt Joiner <anacrolix@gmail.com> from:
|
||||
|
||||
* http://stackoverflow.com/questions/12593576/
|
||||
* https://gist.github.com/anacrolix/3788413
|
||||
|
||||
"""
|
||||
def __init__(self, iterator):
|
||||
#: Iterator/generator for content.
|
||||
self._iter = iterator
|
||||
|
||||
#: Internal buffer.
|
||||
self._left = b''
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def _read1(self, n=None):
|
||||
while not self._left:
|
||||
try:
|
||||
self._left = next(self._iter)
|
||||
except StopIteration:
|
||||
break
|
||||
else:
|
||||
# Make sure we handle text.
|
||||
self._left = force_bytes(self._left)
|
||||
ret = self._left[:n]
|
||||
self._left = self._left[len(ret):]
|
||||
return ret
|
||||
|
||||
def read(self, n=None):
|
||||
"""Return content up to ``n`` length."""
|
||||
l = []
|
||||
if n is None or n < 0:
|
||||
while True:
|
||||
m = self._read1()
|
||||
if not m:
|
||||
break
|
||||
l.append(m)
|
||||
else:
|
||||
while n > 0:
|
||||
m = self._read1(n)
|
||||
if not m:
|
||||
break
|
||||
n -= len(m)
|
||||
l.append(m)
|
||||
return b''.join(l)
|
||||
|
||||
def readline(self):
|
||||
l = []
|
||||
while True:
|
||||
i = self._left.find(b'\n')
|
||||
if i == -1:
|
||||
l.append(self._left)
|
||||
try:
|
||||
self._left = next(self._iter)
|
||||
except StopIteration:
|
||||
self._left = b''
|
||||
break
|
||||
else:
|
||||
l.append(self._left[:i + 1])
|
||||
self._left = self._left[i + 1:]
|
||||
break
|
||||
return b''.join(l)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import tempfile
|
|||
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
from django_downloadview.middlewares import is_download_response
|
||||
from django_downloadview.response import (encode_basename_ascii,
|
||||
|
|
@ -139,9 +140,9 @@ class DownloadResponseValidator(object):
|
|||
test_case.assertTrue(response['Content-Type'].startswith(value))
|
||||
|
||||
def assert_content(self, test_case, response, value):
|
||||
test_case.assertEqual(
|
||||
''.join([s.decode('utf-8') for s in response.streaming_content]),
|
||||
value)
|
||||
"""Assert value equals response's content (byte comparison)."""
|
||||
parts = [force_bytes(s) for s in response.streaming_content]
|
||||
test_case.assertEqual(b''.join(parts), force_bytes(value))
|
||||
|
||||
def assert_attachment(self, test_case, response, value):
|
||||
if value:
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Unit tests."""
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Unit tests around views."""
|
||||
import os
|
||||
import unittest
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
from django.core.files import File
|
||||
from django.http import Http404
|
||||
from django.http.response import HttpResponseNotModified
|
||||
import django.test
|
||||
|
||||
from django_downloadview import exceptions
|
||||
from django_downloadview.test import setup_view
|
||||
from django_downloadview.response import DownloadResponse
|
||||
from django_downloadview import views
|
||||
from django_downloadview.shortcuts import sendfile
|
||||
|
||||
|
||||
class DownloadMixinTestCase(unittest.TestCase):
|
||||
"""Tests around :class:`django_downloadviews.views.base.DownloadMixin`."""
|
||||
def test_get_file(self):
|
||||
"""DownloadMixin.get_file() raise NotImplementedError.
|
||||
|
||||
Subclasses must implement it!
|
||||
|
||||
"""
|
||||
mixin = views.DownloadMixin()
|
||||
with self.assertRaises(NotImplementedError):
|
||||
mixin.get_file()
|
||||
|
||||
def test_get_basename(self):
|
||||
"""DownloadMixin.get_basename() returns basename attribute."""
|
||||
mixin = views.DownloadMixin()
|
||||
self.assertEqual(mixin.get_basename(), None)
|
||||
mixin.basename = 'fake'
|
||||
self.assertEqual(mixin.get_basename(), 'fake')
|
||||
|
||||
def test_was_modified_since_file(self):
|
||||
"""DownloadMixin.was_modified_since() tries (1) file's implementation.
|
||||
|
||||
:meth:`django_downloadview.views.base.DownloadMixin.was_modified_since`
|
||||
first tries to delegate computations to file wrapper's implementation.
|
||||
|
||||
"""
|
||||
file_wrapper = mock.Mock()
|
||||
file_wrapper.was_modified_since = mock.Mock(
|
||||
return_value=mock.sentinel.was_modified)
|
||||
mixin = views.DownloadMixin()
|
||||
self.assertIs(
|
||||
mixin.was_modified_since(file_wrapper, mock.sentinel.since),
|
||||
mock.sentinel.was_modified)
|
||||
file_wrapper.was_modified_since.assertCalledOnceWith(
|
||||
mock.sentinel.since)
|
||||
|
||||
def test_was_modified_since_django(self):
|
||||
"""DownloadMixin.was_modified_since() tries (2) files attributes.
|
||||
|
||||
When calling file wrapper's ``was_modified_since()`` raises
|
||||
``NotImplementedError`` or ``AttributeError``,
|
||||
:meth:`django_downloadview.views.base.DownloadMixin.was_modified_since`
|
||||
tries to pass file wrapper's ``size`` and ``modified_time`` to
|
||||
:func:`django.views.static import was_modified_since`.
|
||||
|
||||
"""
|
||||
file_wrapper = mock.Mock()
|
||||
file_wrapper.was_modified_since = mock.Mock(
|
||||
side_effect=AttributeError)
|
||||
file_wrapper.size = mock.sentinel.size
|
||||
file_wrapper.modified_time = mock.sentinel.modified_time
|
||||
was_modified_since_mock = mock.Mock(
|
||||
return_value=mock.sentinel.was_modified)
|
||||
mixin = views.DownloadMixin()
|
||||
with mock.patch('django_downloadview.views.base.was_modified_since',
|
||||
new=was_modified_since_mock):
|
||||
self.assertIs(
|
||||
mixin.was_modified_since(file_wrapper, mock.sentinel.since),
|
||||
mock.sentinel.was_modified)
|
||||
was_modified_since_mock.assertCalledOnceWith(
|
||||
mock.sentinel.size,
|
||||
mock.sentinel.modified_time)
|
||||
|
||||
def test_was_modified_since_fallback(self):
|
||||
"""DownloadMixin.was_modified_since() fallbacks to `True`.
|
||||
|
||||
When:
|
||||
|
||||
* calling file wrapper's ``was_modified_since()`` raises
|
||||
``NotImplementedError`` or ``AttributeError``;
|
||||
|
||||
* and accessing ``size`` and ``modified_time`` from file wrapper raises
|
||||
``NotImplementedError`` or ``AttributeError``...
|
||||
|
||||
... then
|
||||
:meth:`django_downloadview.views.base.DownloadMixin.was_modified_since`
|
||||
returns ``True``.
|
||||
|
||||
"""
|
||||
file_wrapper = mock.Mock()
|
||||
file_wrapper.was_modified_since = mock.Mock(
|
||||
side_effect=NotImplementedError)
|
||||
type(file_wrapper).modified_time = mock.PropertyMock(
|
||||
side_effect=NotImplementedError)
|
||||
mixin = views.DownloadMixin()
|
||||
self.assertIs(
|
||||
mixin.was_modified_since(file_wrapper, 'fake since'),
|
||||
True)
|
||||
|
||||
def test_not_modified_response(self):
|
||||
"DownloadMixin.not_modified_response returns HttpResponseNotModified."
|
||||
mixin = views.DownloadMixin()
|
||||
response = mixin.not_modified_response()
|
||||
self.assertTrue(isinstance(response, HttpResponseNotModified))
|
||||
|
||||
def test_download_response(self):
|
||||
"DownloadMixin.download_response() returns download response instance."
|
||||
mixin = views.DownloadMixin()
|
||||
mixin.file_instance = mock.sentinel.file_wrapper
|
||||
response_factory = mock.Mock(return_value=mock.sentinel.response)
|
||||
mixin.response_class = response_factory
|
||||
response_kwargs = {'dummy': 'value',
|
||||
'file_instance': mock.sentinel.file_wrapper,
|
||||
'attachment': True,
|
||||
'basename': None,
|
||||
'file_mimetype': None,
|
||||
'file_encoding': None}
|
||||
response = mixin.download_response(**response_kwargs)
|
||||
self.assertIs(response, mock.sentinel.response)
|
||||
response_factory.assert_called_once_with(**response_kwargs) # Not args
|
||||
|
||||
def test_render_to_response_not_modified(self):
|
||||
"""DownloadMixin.render_to_response() respects HTTP_IF_MODIFIED_SINCE
|
||||
header (calls ``not_modified_response()``)."""
|
||||
# Setup.
|
||||
mixin = views.DownloadMixin()
|
||||
mixin.request = django.test.RequestFactory().get(
|
||||
'/dummy-url',
|
||||
HTTP_IF_MODIFIED_SINCE=mock.sentinel.http_if_modified_since)
|
||||
mixin.was_modified_since = mock.Mock(return_value=False)
|
||||
mixin.not_modified_response = mock.Mock(
|
||||
return_value=mock.sentinel.http_not_modified_response)
|
||||
mixin.get_file = mock.Mock(return_value=mock.sentinel.file_wrapper)
|
||||
# Run.
|
||||
response = mixin.render_to_response()
|
||||
# Check.
|
||||
self.assertIs(response, mock.sentinel.http_not_modified_response)
|
||||
mixin.get_file.assert_called_once_with()
|
||||
mixin.was_modified_since.assert_called_once_with(
|
||||
mock.sentinel.file_wrapper,
|
||||
mock.sentinel.http_if_modified_since)
|
||||
mixin.not_modified_response.assert_called_once_with()
|
||||
|
||||
def test_render_to_response_modified(self):
|
||||
"""DownloadMixin.render_to_response() calls download_response()."""
|
||||
# Setup.
|
||||
mixin = views.DownloadMixin()
|
||||
mixin.request = django.test.RequestFactory().get(
|
||||
'/dummy-url',
|
||||
HTTP_IF_MODIFIED_SINCE=None)
|
||||
mixin.was_modified_since = mock.Mock()
|
||||
mixin.download_response = mock.Mock(
|
||||
return_value=mock.sentinel.download_response)
|
||||
mixin.get_file = mock.Mock(return_value=mock.sentinel.file_wrapper)
|
||||
# Run.
|
||||
response = mixin.render_to_response()
|
||||
# Check.
|
||||
self.assertIs(response, mock.sentinel.download_response)
|
||||
mixin.get_file.assert_called_once_with()
|
||||
self.assertEqual(mixin.was_modified_since.call_count, 0)
|
||||
mixin.download_response.assert_called_once_with()
|
||||
|
||||
def test_render_to_response_file_not_found(self):
|
||||
"DownloadMixin.render_to_response() calls file_not_found_response()."
|
||||
# Setup.
|
||||
mixin = views.DownloadMixin()
|
||||
mixin.request = django.test.RequestFactory().get('/dummy-url')
|
||||
mixin.get_file = mock.Mock(side_effect=exceptions.FileNotFound)
|
||||
mixin.file_not_found_response = mock.Mock()
|
||||
# Run.
|
||||
mixin.render_to_response()
|
||||
# Check.
|
||||
mixin.file_not_found_response.assert_called_once_with()
|
||||
|
||||
def test_file_not_found_response(self):
|
||||
"""DownloadMixin.file_not_found_response() raises Http404."""
|
||||
mixin = views.DownloadMixin()
|
||||
with self.assertRaises(Http404):
|
||||
mixin.file_not_found_response()
|
||||
|
||||
|
||||
class BaseDownloadViewTestCase(unittest.TestCase):
|
||||
"Tests around :class:`django_downloadviews.views.base.BaseDownloadView`."
|
||||
def test_get(self):
|
||||
"""BaseDownloadView.get() calls render_to_response()."""
|
||||
request = django.test.RequestFactory().get('/dummy-url')
|
||||
args = ['dummy-arg']
|
||||
kwargs = {'dummy': 'kwarg'}
|
||||
view = setup_view(views.BaseDownloadView(), request, *args, **kwargs)
|
||||
view.render_to_response = mock.Mock(
|
||||
return_value=mock.sentinel.response)
|
||||
response = view.get(request, *args, **kwargs)
|
||||
self.assertIs(response, mock.sentinel.response)
|
||||
view.render_to_response.assert_called_once_with()
|
||||
|
||||
|
||||
class PathDownloadViewTestCase(unittest.TestCase):
|
||||
"Tests for :class:`django_downloadviews.views.path.PathDownloadView`."
|
||||
def test_get_file_ok(self):
|
||||
"PathDownloadView.get_file() returns ``File`` instance."
|
||||
view = setup_view(views.PathDownloadView(path=__file__),
|
||||
'fake request')
|
||||
file_wrapper = view.get_file()
|
||||
self.assertTrue(isinstance(file_wrapper, File))
|
||||
|
||||
def test_get_file_does_not_exist(self):
|
||||
"""PathDownloadView.get_file() raises FileNotFound if field does not
|
||||
exist.
|
||||
|
||||
"""
|
||||
view = setup_view(views.PathDownloadView(path='i-do-no-exist'),
|
||||
'fake request')
|
||||
with self.assertRaises(exceptions.FileNotFound):
|
||||
view.get_file()
|
||||
|
||||
def test_get_file_is_directory(self):
|
||||
"""PathDownloadView.get_file() raises FileNotFound if file is a
|
||||
directory."""
|
||||
view = setup_view(
|
||||
views.PathDownloadView(path=os.path.dirname(__file__)),
|
||||
'fake request')
|
||||
with self.assertRaises(exceptions.FileNotFound):
|
||||
view.get_file()
|
||||
|
||||
|
||||
class ObjectDownloadViewTestCase(unittest.TestCase):
|
||||
"Tests for :class:`django_downloadviews.views.object.ObjectDownloadView`."
|
||||
def test_get_file_ok(self):
|
||||
"ObjectDownloadView.get_file() returns ``file`` field by default."
|
||||
view = setup_view(views.ObjectDownloadView(), 'fake request')
|
||||
view.object = mock.Mock(spec=['file'])
|
||||
view.get_file()
|
||||
|
||||
def test_get_file_wrong_field(self):
|
||||
"""ObjectDownloadView.get_file() raises AttributeError if field does
|
||||
not exist.
|
||||
|
||||
``AttributeError`` is expected because this is a configuration error,
|
||||
i.e. it is related to Python code.
|
||||
|
||||
"""
|
||||
view = setup_view(views.ObjectDownloadView(file_field='other_field'),
|
||||
'fake request')
|
||||
view.object = mock.Mock(spec=['file'])
|
||||
with self.assertRaises(AttributeError):
|
||||
view.get_file()
|
||||
|
||||
def test_get_file_empty_field(self):
|
||||
"""ObjectDownloadView.get_file() raises FileNotFound if field does not
|
||||
exist."""
|
||||
view = setup_view(views.ObjectDownloadView(file_field='other_field'),
|
||||
'fake request')
|
||||
view.object = mock.Mock()
|
||||
view.object.other_field = None
|
||||
with self.assertRaises(exceptions.FileNotFound):
|
||||
view.get_file()
|
||||
|
||||
|
||||
class SendfileTestCase(django.test.TestCase):
|
||||
"""Tests around :func:`django_downloadview.sendfile.sendfile`."""
|
||||
def test_defaults(self):
|
||||
"""sendfile() takes at least request and filename."""
|
||||
request = django.test.RequestFactory().get('/fake-url')
|
||||
filename = __file__
|
||||
response = sendfile(request, filename)
|
||||
self.assertTrue(isinstance(response, DownloadResponse))
|
||||
self.assertFalse(response.attachment)
|
||||
|
||||
def test_custom(self):
|
||||
"""sendfile() accepts various arguments for response tuning."""
|
||||
request = django.test.RequestFactory().get('/fake-url')
|
||||
filename = __file__
|
||||
response = sendfile(request,
|
||||
filename,
|
||||
attachment=True,
|
||||
attachment_filename='toto.txt',
|
||||
mimetype='test/octet-stream',
|
||||
encoding='gzip')
|
||||
self.assertTrue(isinstance(response, DownloadResponse))
|
||||
self.assertTrue(response.attachment)
|
||||
self.assertEqual(response.basename, 'toto.txt')
|
||||
self.assertEqual(response['Content-Type'],
|
||||
'test/octet-stream; charset=utf-8')
|
||||
self.assertEqual(response.get_encoding(), 'gzip')
|
||||
|
||||
def test_404(self):
|
||||
"""sendfile() raises Http404 if file does not exists."""
|
||||
request = django.test.RequestFactory().get('/fake-url')
|
||||
filename = 'i-do-no-exist'
|
||||
with self.assertRaises(Http404):
|
||||
sendfile(request, filename)
|
||||
|
|
@ -19,8 +19,8 @@ optimizations.
|
|||
|
||||
* :doc:`/about/alternatives`
|
||||
* `roadmap
|
||||
<https://github.com/benoitbryon/django-downloadview/issues/milestones>`_
|
||||
<https://github.com/benoitbryon/django-downloadview/milestones>`_
|
||||
|
||||
.. target-notes::
|
||||
|
||||
.. _`Django`: https://django-project.com
|
||||
.. _`Django`: https://djangoproject.com
|
||||
|
|
|
|||
233
docs/conf.py
233
docs/conf.py
|
|
@ -1,16 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# django-downloadview documentation build configuration file, created by
|
||||
# sphinx-quickstart on Mon Aug 27 11:37:23 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
"""django-downloadview documentation build configuration file."""
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
# Minimal Django settings. Required to use sphinx.ext.autodoc, because
|
||||
|
|
@ -23,28 +14,16 @@ settings.configure(
|
|||
)
|
||||
|
||||
|
||||
doc_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_dir = os.path.dirname(doc_dir)
|
||||
version_filename = os.path.join(project_dir, 'VERSION')
|
||||
# -- General configuration ----------------------------------------------------
|
||||
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.autosummary',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.intersphinx']
|
||||
# Extensions.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.autosummary',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.intersphinx',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
|
@ -53,102 +32,55 @@ templates_path = ['_templates']
|
|||
source_suffix = '.txt'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
source_encoding = 'utf-8'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'django-downloadview'
|
||||
copyright = u'2012, Benoît Bryon'
|
||||
project_slug = re.sub(r'([\w_.-]+)', u'-', project)
|
||||
copyright = u'2012-2015, Benoît Bryon'
|
||||
author = u'Benoît Bryon'
|
||||
author_slug = re.sub(r'([\w_.-]+)', u'-', author)
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = open(version_filename).read().strip()
|
||||
configuration_dir = os.path.dirname(__file__)
|
||||
documentation_dir = configuration_dir
|
||||
version_file = os.path.normpath(os.path.join(
|
||||
documentation_dir,
|
||||
'../VERSION'))
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
release = open(version_file).read().strip()
|
||||
# The short X.Y version.
|
||||
version = '.'.join(release.split('.')[0:1])
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
language = 'en'
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
html_sidebars = {
|
||||
'**': ['globaltoc.html',
|
||||
|
|
@ -157,119 +89,60 @@ html_sidebars = {
|
|||
'searchbox.html'],
|
||||
}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'django-downloadviewdoc'
|
||||
htmlhelp_basename = u'{project}doc'.format(project=project_slug)
|
||||
|
||||
|
||||
# -- Options for sphinx.ext.intersphinx ---------------------------------------
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('http://docs.python.org/2.7', None),
|
||||
'django': ('http://docs.djangoproject.com/en/1.5/',
|
||||
'http://docs.djangoproject.com/en/1.5/_objects/'),
|
||||
'django': ('http://docs.djangoproject.com/en/1.8/',
|
||||
'http://docs.djangoproject.com/en/1.8/_objects/'),
|
||||
'requests': ('http://docs.python-requests.org/en/latest/', None),
|
||||
}
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# -- Options for LaTeX output -------------------------------------------------
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
latex_elements = {}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
# (source start file, target name, title, author, documentclass
|
||||
# [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'django-downloadview.tex', u'django-downloadview Documentation',
|
||||
u'Benoît Bryon', 'manual'),
|
||||
('index',
|
||||
u'{project}.tex'.format(project=project_slug),
|
||||
u'{project} Documentation'.format(project=project),
|
||||
author,
|
||||
'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
# -- Options for manual page output -------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'django-downloadview', u'django-downloadview Documentation',
|
||||
[u'Benoît Bryon'], 1)
|
||||
('index',
|
||||
project,
|
||||
u'{project} Documentation'.format(project=project),
|
||||
[author],
|
||||
1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
# -- Options for Texinfo output -----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'django-downloadview', u'django-downloadview Documentation',
|
||||
u'Benoît Bryon', 'django-downloadview', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
('index',
|
||||
project_slug,
|
||||
u'{project} Documentation'.format(project=project),
|
||||
author,
|
||||
project,
|
||||
'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
|
|
|||
|
|
@ -48,6 +48,27 @@ django-downloadview builtins
|
|||
This is a convenient wrapper to use in :doc:`/views/virtual` subclasses.
|
||||
|
||||
|
||||
**********************
|
||||
Low-level IO utilities
|
||||
**********************
|
||||
|
||||
`django-downloadview` provides two classes to implement file-like objects
|
||||
whose content is dynamically generated:
|
||||
|
||||
* :class:`~django_downloadview.io.TextIteratorIO` for generated text;
|
||||
* :class:`~django_downloadview.io.BytesIteratorIO` for generated bytes.
|
||||
|
||||
These classes may be handy to serve dynamically generated files. See
|
||||
:doc:`/views/virtual` for details.
|
||||
|
||||
.. tip::
|
||||
|
||||
**Text or bytes?** (formerly "unicode or str?") As `django-downloadview`
|
||||
is meant to serve files, as opposed to read or parse files, what matters
|
||||
is file contents is preserved. `django-downloadview` tends to handle files
|
||||
in binary mode and as bytes.
|
||||
|
||||
|
||||
*************
|
||||
API reference
|
||||
*************
|
||||
|
|
@ -81,6 +102,26 @@ VirtualFile
|
|||
:member-order: bysource
|
||||
|
||||
|
||||
BytesIteratorIO
|
||||
===============
|
||||
|
||||
.. autoclass:: django_downloadview.io.BytesIteratorIO
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
TextIteratorIO
|
||||
==============
|
||||
|
||||
.. autoclass:: django_downloadview.io.TextIteratorIO
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
:member-order: bysource
|
||||
|
||||
|
||||
.. rubric:: Notes & references
|
||||
|
||||
.. target-notes::
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ Overview, concepts
|
|||
|
||||
Given:
|
||||
|
||||
* you manage files with Django (permissions, search, generation, ...)
|
||||
* you manage files with Django (permissions, filters, generation, ...)
|
||||
|
||||
* files are stored somewhere or generated somehow (local filesystem, remote
|
||||
storage, memory...)
|
||||
|
||||
As a developer, you want to serve files quick and efficiently.
|
||||
|
||||
Here is an overview of ``django-downloadview``'s answer...
|
||||
Here is an overview of `django-downloadview`'s answer...
|
||||
|
||||
|
||||
************************************
|
||||
|
|
@ -23,9 +23,8 @@ Choose the generic view depending on the file you want to serve:
|
|||
* :doc:`/views/object`: file field in a model;
|
||||
* :doc:`/views/storage`: file in a storage;
|
||||
* :doc:`/views/path`: absolute filename on local filesystem;
|
||||
* :doc:`/views/http`: URL (the resource is proxied);
|
||||
* :doc:`/views/virtual`: bytes, text, :class:`~StringIO.StringIO`, generated
|
||||
file...
|
||||
* :doc:`/views/http`: file at URL (the resource is proxied);
|
||||
* :doc:`/views/virtual`: bytes, text, file-like objects, generated files...
|
||||
|
||||
|
||||
*************************************************
|
||||
|
|
@ -67,14 +66,15 @@ Learn more about available file wrappers in :doc:`files`.
|
|||
Middlewares convert DownloadResponse into ProxiedDownloadResponse
|
||||
*****************************************************************
|
||||
|
||||
Before WSGI application use file wrapper to load file contents, middlewares
|
||||
(or decorators) are given the opportunity to capture
|
||||
Before WSGI application use file wrapper and actually use file contents,
|
||||
middlewares or decorators) are given the opportunity to capture
|
||||
:class:`~django_downloadview.response.DownloadResponse` instances.
|
||||
|
||||
Let's take this opportunity to optimize file loading and streaming!
|
||||
|
||||
A good optimization it to delegate streaming to a reverse proxy, such as
|
||||
`nginx`_ via `X-Accel`_ internal redirects.
|
||||
`nginx`_ via `X-Accel`_ internal redirects. This way, Django doesn't load file
|
||||
content in memory.
|
||||
|
||||
`django_downloadview` provides middlewares that convert
|
||||
:class:`~django_downloadview.response.DownloadResponse` into
|
||||
|
|
|
|||
|
|
@ -2,23 +2,22 @@
|
|||
Configure
|
||||
#########
|
||||
|
||||
Here is the list of settings used by `django-downloadview`.
|
||||
Here is the list of Django settings for `django-downloadview`.
|
||||
|
||||
|
||||
**************
|
||||
INSTALLED_APPS
|
||||
**************
|
||||
|
||||
There is no need to register this application in your Django's
|
||||
``INSTALLED_APPS`` setting.
|
||||
There is no need to register this application in ``INSTALLED_APPS``.
|
||||
|
||||
|
||||
******************
|
||||
MIDDLEWARE_CLASSES
|
||||
******************
|
||||
|
||||
If you plan to setup reverse-proxy optimizations, add
|
||||
``django_downloadview.SmartDownloadMiddleware`` to ``MIDDLEWARE_CLASSES``.
|
||||
If you plan to setup :doc:`reverse-proxy optimizations </optimizations/index>`,
|
||||
add ``django_downloadview.SmartDownloadMiddleware`` to ``MIDDLEWARE_CLASSES``.
|
||||
It is a response middleware. Move it after middlewares that compute the
|
||||
response content such as gzip middleware.
|
||||
|
||||
|
|
@ -26,7 +25,8 @@ Example:
|
|||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 64-71
|
||||
:start-after: BEGIN middlewares
|
||||
:end-before: END middlewares
|
||||
|
||||
|
||||
********************
|
||||
|
|
@ -43,7 +43,8 @@ Example:
|
|||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 75
|
||||
:start-after: BEGIN backend
|
||||
:end-before: END backend
|
||||
|
||||
See :doc:`/optimizations/index` for a list of available backends (middlewares).
|
||||
|
||||
|
|
@ -69,7 +70,8 @@ Here is an example containing one rule using keyword arguments:
|
|||
|
||||
.. literalinclude:: /../demo/demoproject/settings.py
|
||||
:language: python
|
||||
:lines: 80, 81-84, 103
|
||||
:start-after: BEGIN rules
|
||||
:end-before: END rules
|
||||
|
||||
See :doc:`/optimizations/index` for details about builtin backends
|
||||
(middlewares) and their options.
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ it returns a suitable file wrapper...
|
|||
.. note::
|
||||
|
||||
Current implementation does not support reverse-proxy optimizations,
|
||||
because there is no place reverse-proxy can load files from after Django
|
||||
exited.
|
||||
because content is actually generated within Django, not stored in some
|
||||
third-party place.
|
||||
|
||||
|
||||
************
|
||||
|
|
@ -68,7 +68,7 @@ Let's consider you have a generator function (``yield``) or an iterator object
|
|||
|
||||
Stream generated content using :class:`VirtualDownloadView`,
|
||||
:class:`~django_downloadview.files.VirtualFile` and
|
||||
:class:`~django_downloadview.file.StringIteratorIO`:
|
||||
:class:`~django_downloadview.io.BytesIteratorIO`:
|
||||
|
||||
.. literalinclude:: /../demo/demoproject/virtual/views.py
|
||||
:language: python
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
[nosetests]
|
||||
verbosity = 2
|
||||
nocapture = True
|
||||
with-doctest = True
|
||||
rednose = True
|
||||
no-path-adjustment = True
|
||||
all-modules = True
|
||||
cover-inclusive = True
|
||||
cover-tests = True
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
[nosetests]
|
||||
with-coverage = True
|
||||
cover-package = demoproject
|
||||
tests = demoproject
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
[nosetests]
|
||||
with-coverage = True
|
||||
cover-package = django_downloadview
|
||||
tests = django_downloadview,tests
|
||||
71
setup.py
71
setup.py
|
|
@ -1,9 +1,24 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Python packaging."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
class Tox(TestCommand):
|
||||
"""Test command that runs tox."""
|
||||
def finalize_options(self):
|
||||
TestCommand.finalize_options(self)
|
||||
self.test_args = []
|
||||
self.test_suite = True
|
||||
|
||||
def run_tests(self):
|
||||
import tox # import here, cause outside the eggs aren't loaded.
|
||||
errno = tox.cmdline(self.test_args)
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
#: Absolute path to directory containing setup.py file.
|
||||
|
|
@ -18,8 +33,9 @@ README = open(os.path.join(here, 'README.rst')).read()
|
|||
VERSION = open(os.path.join(here, 'VERSION')).read().strip()
|
||||
AUTHOR = u'Benoît Bryon'
|
||||
EMAIL = 'benoit@marmelune.net'
|
||||
LICENSE = 'BSD'
|
||||
URL = 'https://{name}.readthedocs.org/'.format(name=NAME)
|
||||
CLASSIFIERS = ['Development Status :: 4 - Beta',
|
||||
CLASSIFIERS = ['Development Status :: 5 - Production/Stable',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
|
|
@ -36,25 +52,42 @@ KEYWORDS = ['file',
|
|||
'mod_xsendfile',
|
||||
'offload']
|
||||
PACKAGES = [NAME.replace('-', '_')]
|
||||
REQUIREMENTS = ['setuptools', 'Django>=1.5', 'requests', 'six']
|
||||
if IS_PYTHON2:
|
||||
REQUIREMENTS.append('mock')
|
||||
REQUIREMENTS = [
|
||||
# BEGIN requirements
|
||||
'Django>=1.5',
|
||||
'requests',
|
||||
'setuptools',
|
||||
'six',
|
||||
# END requirements
|
||||
]
|
||||
ENTRY_POINTS = {}
|
||||
SETUP_REQUIREMENTS = ['setuptools']
|
||||
TEST_REQUIREMENTS = ['tox']
|
||||
CMDCLASS = {'test': Tox}
|
||||
EXTRA_REQUIREMENTS = {
|
||||
'test': TEST_REQUIREMENTS,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__': # Don't run setup() when we import this module.
|
||||
setup(name=NAME,
|
||||
version=VERSION,
|
||||
description=DESCRIPTION,
|
||||
long_description=README,
|
||||
classifiers=CLASSIFIERS,
|
||||
keywords=' '.join(KEYWORDS),
|
||||
author=AUTHOR,
|
||||
author_email=EMAIL,
|
||||
url=URL,
|
||||
license='BSD',
|
||||
packages=PACKAGES,
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=REQUIREMENTS,
|
||||
entry_points=ENTRY_POINTS)
|
||||
setup(
|
||||
name=NAME,
|
||||
version=VERSION,
|
||||
description=DESCRIPTION,
|
||||
long_description=README,
|
||||
classifiers=CLASSIFIERS,
|
||||
keywords=' '.join(KEYWORDS),
|
||||
author=AUTHOR,
|
||||
author_email=EMAIL,
|
||||
url=URL,
|
||||
license=LICENSE,
|
||||
packages=PACKAGES,
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=REQUIREMENTS,
|
||||
entry_points=ENTRY_POINTS,
|
||||
tests_require=TEST_REQUIREMENTS,
|
||||
cmdclass=CMDCLASS,
|
||||
setup_requires=SETUP_REQUIREMENTS,
|
||||
extras_require=EXTRA_REQUIREMENTS,
|
||||
)
|
||||
|
|
|
|||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
53
tests/io.py
Normal file
53
tests/io.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# coding=utf-8
|
||||
"""Tests around :mod:`django_downloadview.io`."""
|
||||
import unittest
|
||||
|
||||
from django_downloadview import TextIteratorIO, BytesIteratorIO
|
||||
|
||||
|
||||
HELLO_TEXT = u'Hello world!\né\n'
|
||||
HELLO_BYTES = b'Hello world!\n\xc3\xa9\n'
|
||||
|
||||
|
||||
def generate_hello_text():
|
||||
"""Generate u'Hello world!\n'."""
|
||||
yield u'Hello '
|
||||
yield u'world!'
|
||||
yield u'\n'
|
||||
yield u'é'
|
||||
yield u'\n'
|
||||
|
||||
|
||||
def generate_hello_bytes():
|
||||
"""Generate b'Hello world!\n'."""
|
||||
yield b'Hello '
|
||||
yield b'world!'
|
||||
yield b'\n'
|
||||
yield b'\xc3\xa9'
|
||||
yield b'\n'
|
||||
|
||||
|
||||
class TextIteratorIOTestCase(unittest.TestCase):
|
||||
"""Tests around :class:`~django_downloadview.io.TextIteratorIO`."""
|
||||
def test_read_text(self):
|
||||
"""TextIteratorIO obviously accepts text generator."""
|
||||
file_obj = TextIteratorIO(generate_hello_text())
|
||||
self.assertEqual(file_obj.read(), HELLO_TEXT)
|
||||
|
||||
def test_read_bytes(self):
|
||||
"""TextIteratorIO converts bytes as text."""
|
||||
file_obj = TextIteratorIO(generate_hello_bytes())
|
||||
self.assertEqual(file_obj.read(), HELLO_TEXT)
|
||||
|
||||
|
||||
class BytesIteratorIOTestCase(unittest.TestCase):
|
||||
"""Tests around :class:`~django_downloadview.io.BytesIteratorIO`."""
|
||||
def test_read_bytes(self):
|
||||
"""BytesIteratorIO obviously accepts bytes generator."""
|
||||
file_obj = BytesIteratorIO(generate_hello_bytes())
|
||||
self.assertEqual(file_obj.read(), HELLO_BYTES)
|
||||
|
||||
def test_read_text(self):
|
||||
"""BytesIteratorIO converts text as bytes."""
|
||||
file_obj = BytesIteratorIO(generate_hello_text())
|
||||
self.assertEqual(file_obj.read(), HELLO_BYTES)
|
||||
42
tests/sendfile.py
Normal file
42
tests/sendfile.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# coding=utf-8
|
||||
"""Tests around :py:mod:`django_downloadview.sendfile`."""
|
||||
from django.http import Http404
|
||||
import django.test
|
||||
|
||||
from django_downloadview.response import DownloadResponse
|
||||
from django_downloadview.shortcuts import sendfile
|
||||
|
||||
|
||||
class SendfileTestCase(django.test.TestCase):
|
||||
"""Tests around :func:`django_downloadview.sendfile.sendfile`."""
|
||||
def test_defaults(self):
|
||||
"""sendfile() takes at least request and filename."""
|
||||
request = django.test.RequestFactory().get('/fake-url')
|
||||
filename = __file__
|
||||
response = sendfile(request, filename)
|
||||
self.assertTrue(isinstance(response, DownloadResponse))
|
||||
self.assertFalse(response.attachment)
|
||||
|
||||
def test_custom(self):
|
||||
"""sendfile() accepts various arguments for response tuning."""
|
||||
request = django.test.RequestFactory().get('/fake-url')
|
||||
filename = __file__
|
||||
response = sendfile(request,
|
||||
filename,
|
||||
attachment=True,
|
||||
attachment_filename='toto.txt',
|
||||
mimetype='test/octet-stream',
|
||||
encoding='gzip')
|
||||
self.assertTrue(isinstance(response, DownloadResponse))
|
||||
self.assertTrue(response.attachment)
|
||||
self.assertEqual(response.basename, 'toto.txt')
|
||||
self.assertEqual(response['Content-Type'],
|
||||
'test/octet-stream; charset=utf-8')
|
||||
self.assertEqual(response.get_encoding(), 'gzip')
|
||||
|
||||
def test_404(self):
|
||||
"""sendfile() raises Http404 if file does not exists."""
|
||||
request = django.test.RequestFactory().get('/fake-url')
|
||||
filename = 'i-do-no-exist'
|
||||
with self.assertRaises(Http404):
|
||||
sendfile(request, filename)
|
||||
270
tests/views.py
270
tests/views.py
|
|
@ -1,29 +1,41 @@
|
|||
# coding=utf-8
|
||||
"""Tests around :py:mod:`django_downloadview.views`."""
|
||||
"""Tests around :mod:`django_downloadview.views`."""
|
||||
import os
|
||||
import unittest
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
|
||||
from django.core.files import File
|
||||
from django.http import Http404
|
||||
from django.http.response import HttpResponseNotModified
|
||||
import django.test
|
||||
|
||||
from django_downloadview import exceptions
|
||||
from django_downloadview.test import setup_view
|
||||
from django_downloadview import views
|
||||
|
||||
|
||||
def setup_view(view, request, *args, **kwargs):
|
||||
"""Mimic as_view() returned callable, but returns view instance.
|
||||
|
||||
``*args`` and ``**kwargs`` are the same you would pass to ``reverse()``
|
||||
|
||||
"""
|
||||
view.request = request
|
||||
view.args = args
|
||||
view.kwargs = kwargs
|
||||
return view
|
||||
|
||||
|
||||
class DownloadMixinTestCase(unittest.TestCase):
|
||||
"""Test suite around :py:class:`django_downloadview.views.DownloadMixin`.
|
||||
"""
|
||||
"""Test suite around :class:`django_downloadview.views.DownloadMixin`."""
|
||||
def test_get_file(self):
|
||||
"""DownloadMixin.get_file() raise NotImplementedError.
|
||||
|
||||
Subclasses must implement it!
|
||||
|
||||
"""
|
||||
mixin = views.DownloadMixin()
|
||||
with self.assertRaises(NotImplementedError):
|
||||
mixin.get_file()
|
||||
|
||||
def test_get_basename(self):
|
||||
"""DownloadMixin.get_basename() returns basename attribute."""
|
||||
mixin = views.DownloadMixin()
|
||||
self.assertEqual(mixin.get_basename(), None)
|
||||
mixin.basename = 'fake'
|
||||
self.assertEqual(mixin.get_basename(), 'fake')
|
||||
|
||||
def test_was_modified_since_specific(self):
|
||||
"""DownloadMixin.was_modified_since() delegates to file wrapper."""
|
||||
file_wrapper = mock.Mock()
|
||||
|
|
@ -49,6 +61,234 @@ class DownloadMixinTestCase(unittest.TestCase):
|
|||
since = mock.sentinel.since
|
||||
self.assertTrue(mixin.was_modified_since(file_wrapper, since))
|
||||
|
||||
def test_was_modified_since_file(self):
|
||||
"""DownloadMixin.was_modified_since() tries (1) file's implementation.
|
||||
|
||||
:meth:`django_downloadview.views.base.DownloadMixin.was_modified_since`
|
||||
first tries to delegate computations to file wrapper's implementation.
|
||||
|
||||
"""
|
||||
file_wrapper = mock.Mock()
|
||||
file_wrapper.was_modified_since = mock.Mock(
|
||||
return_value=mock.sentinel.was_modified)
|
||||
mixin = views.DownloadMixin()
|
||||
self.assertIs(
|
||||
mixin.was_modified_since(file_wrapper, mock.sentinel.since),
|
||||
mock.sentinel.was_modified)
|
||||
file_wrapper.was_modified_since.assertCalledOnceWith(
|
||||
mock.sentinel.since)
|
||||
|
||||
def test_was_modified_since_django(self):
|
||||
"""DownloadMixin.was_modified_since() tries (2) files attributes.
|
||||
|
||||
When calling file wrapper's ``was_modified_since()`` raises
|
||||
``NotImplementedError`` or ``AttributeError``,
|
||||
:meth:`django_downloadview.views.base.DownloadMixin.was_modified_since`
|
||||
tries to pass file wrapper's ``size`` and ``modified_time`` to
|
||||
:func:`django.views.static import was_modified_since`.
|
||||
|
||||
"""
|
||||
file_wrapper = mock.Mock()
|
||||
file_wrapper.was_modified_since = mock.Mock(
|
||||
side_effect=AttributeError)
|
||||
file_wrapper.size = mock.sentinel.size
|
||||
file_wrapper.modified_time = mock.sentinel.modified_time
|
||||
was_modified_since_mock = mock.Mock(
|
||||
return_value=mock.sentinel.was_modified)
|
||||
mixin = views.DownloadMixin()
|
||||
with mock.patch('django_downloadview.views.base.was_modified_since',
|
||||
new=was_modified_since_mock):
|
||||
self.assertIs(
|
||||
mixin.was_modified_since(file_wrapper, mock.sentinel.since),
|
||||
mock.sentinel.was_modified)
|
||||
was_modified_since_mock.assertCalledOnceWith(
|
||||
mock.sentinel.size,
|
||||
mock.sentinel.modified_time)
|
||||
|
||||
def test_was_modified_since_fallback(self):
|
||||
"""DownloadMixin.was_modified_since() fallbacks to `True`.
|
||||
|
||||
When:
|
||||
|
||||
* calling file wrapper's ``was_modified_since()`` raises
|
||||
``NotImplementedError`` or ``AttributeError``;
|
||||
|
||||
* and accessing ``size`` and ``modified_time`` from file wrapper raises
|
||||
``NotImplementedError`` or ``AttributeError``...
|
||||
|
||||
... then
|
||||
:meth:`django_downloadview.views.base.DownloadMixin.was_modified_since`
|
||||
returns ``True``.
|
||||
|
||||
"""
|
||||
file_wrapper = mock.Mock()
|
||||
file_wrapper.was_modified_since = mock.Mock(
|
||||
side_effect=NotImplementedError)
|
||||
type(file_wrapper).modified_time = mock.PropertyMock(
|
||||
side_effect=NotImplementedError)
|
||||
mixin = views.DownloadMixin()
|
||||
self.assertIs(
|
||||
mixin.was_modified_since(file_wrapper, 'fake since'),
|
||||
True)
|
||||
|
||||
def test_not_modified_response(self):
|
||||
"DownloadMixin.not_modified_response returns HttpResponseNotModified."
|
||||
mixin = views.DownloadMixin()
|
||||
response = mixin.not_modified_response()
|
||||
self.assertTrue(isinstance(response, HttpResponseNotModified))
|
||||
|
||||
def test_download_response(self):
|
||||
"DownloadMixin.download_response() returns download response instance."
|
||||
mixin = views.DownloadMixin()
|
||||
mixin.file_instance = mock.sentinel.file_wrapper
|
||||
response_factory = mock.Mock(return_value=mock.sentinel.response)
|
||||
mixin.response_class = response_factory
|
||||
response_kwargs = {'dummy': 'value',
|
||||
'file_instance': mock.sentinel.file_wrapper,
|
||||
'attachment': True,
|
||||
'basename': None,
|
||||
'file_mimetype': None,
|
||||
'file_encoding': None}
|
||||
response = mixin.download_response(**response_kwargs)
|
||||
self.assertIs(response, mock.sentinel.response)
|
||||
response_factory.assert_called_once_with(**response_kwargs) # Not args
|
||||
|
||||
def test_render_to_response_not_modified(self):
|
||||
"""DownloadMixin.render_to_response() respects HTTP_IF_MODIFIED_SINCE
|
||||
header (calls ``not_modified_response()``)."""
|
||||
# Setup.
|
||||
mixin = views.DownloadMixin()
|
||||
mixin.request = django.test.RequestFactory().get(
|
||||
'/dummy-url',
|
||||
HTTP_IF_MODIFIED_SINCE=mock.sentinel.http_if_modified_since)
|
||||
mixin.was_modified_since = mock.Mock(return_value=False)
|
||||
mixin.not_modified_response = mock.Mock(
|
||||
return_value=mock.sentinel.http_not_modified_response)
|
||||
mixin.get_file = mock.Mock(return_value=mock.sentinel.file_wrapper)
|
||||
# Run.
|
||||
response = mixin.render_to_response()
|
||||
# Check.
|
||||
self.assertIs(response, mock.sentinel.http_not_modified_response)
|
||||
mixin.get_file.assert_called_once_with()
|
||||
mixin.was_modified_since.assert_called_once_with(
|
||||
mock.sentinel.file_wrapper,
|
||||
mock.sentinel.http_if_modified_since)
|
||||
mixin.not_modified_response.assert_called_once_with()
|
||||
|
||||
def test_render_to_response_modified(self):
|
||||
"""DownloadMixin.render_to_response() calls download_response()."""
|
||||
# Setup.
|
||||
mixin = views.DownloadMixin()
|
||||
mixin.request = django.test.RequestFactory().get(
|
||||
'/dummy-url',
|
||||
HTTP_IF_MODIFIED_SINCE=None)
|
||||
mixin.was_modified_since = mock.Mock()
|
||||
mixin.download_response = mock.Mock(
|
||||
return_value=mock.sentinel.download_response)
|
||||
mixin.get_file = mock.Mock(return_value=mock.sentinel.file_wrapper)
|
||||
# Run.
|
||||
response = mixin.render_to_response()
|
||||
# Check.
|
||||
self.assertIs(response, mock.sentinel.download_response)
|
||||
mixin.get_file.assert_called_once_with()
|
||||
self.assertEqual(mixin.was_modified_since.call_count, 0)
|
||||
mixin.download_response.assert_called_once_with()
|
||||
|
||||
def test_render_to_response_file_not_found(self):
|
||||
"DownloadMixin.render_to_response() calls file_not_found_response()."
|
||||
# Setup.
|
||||
mixin = views.DownloadMixin()
|
||||
mixin.request = django.test.RequestFactory().get('/dummy-url')
|
||||
mixin.get_file = mock.Mock(side_effect=exceptions.FileNotFound)
|
||||
mixin.file_not_found_response = mock.Mock()
|
||||
# Run.
|
||||
mixin.render_to_response()
|
||||
# Check.
|
||||
mixin.file_not_found_response.assert_called_once_with()
|
||||
|
||||
def test_file_not_found_response(self):
|
||||
"""DownloadMixin.file_not_found_response() raises Http404."""
|
||||
mixin = views.DownloadMixin()
|
||||
with self.assertRaises(Http404):
|
||||
mixin.file_not_found_response()
|
||||
|
||||
|
||||
class BaseDownloadViewTestCase(unittest.TestCase):
|
||||
"Tests around :class:`django_downloadviews.views.base.BaseDownloadView`."
|
||||
def test_get(self):
|
||||
"""BaseDownloadView.get() calls render_to_response()."""
|
||||
request = django.test.RequestFactory().get('/dummy-url')
|
||||
args = ['dummy-arg']
|
||||
kwargs = {'dummy': 'kwarg'}
|
||||
view = setup_view(views.BaseDownloadView(), request, *args, **kwargs)
|
||||
view.render_to_response = mock.Mock(
|
||||
return_value=mock.sentinel.response)
|
||||
response = view.get(request, *args, **kwargs)
|
||||
self.assertIs(response, mock.sentinel.response)
|
||||
view.render_to_response.assert_called_once_with()
|
||||
|
||||
|
||||
class PathDownloadViewTestCase(unittest.TestCase):
|
||||
"Tests for :class:`django_downloadviews.views.path.PathDownloadView`."
|
||||
def test_get_file_ok(self):
|
||||
"PathDownloadView.get_file() returns ``File`` instance."
|
||||
view = setup_view(views.PathDownloadView(path=__file__),
|
||||
'fake request')
|
||||
file_wrapper = view.get_file()
|
||||
self.assertTrue(isinstance(file_wrapper, File))
|
||||
|
||||
def test_get_file_does_not_exist(self):
|
||||
"""PathDownloadView.get_file() raises FileNotFound if field does not
|
||||
exist.
|
||||
|
||||
"""
|
||||
view = setup_view(views.PathDownloadView(path='i-do-no-exist'),
|
||||
'fake request')
|
||||
with self.assertRaises(exceptions.FileNotFound):
|
||||
view.get_file()
|
||||
|
||||
def test_get_file_is_directory(self):
|
||||
"""PathDownloadView.get_file() raises FileNotFound if file is a
|
||||
directory."""
|
||||
view = setup_view(
|
||||
views.PathDownloadView(path=os.path.dirname(__file__)),
|
||||
'fake request')
|
||||
with self.assertRaises(exceptions.FileNotFound):
|
||||
view.get_file()
|
||||
|
||||
|
||||
class ObjectDownloadViewTestCase(unittest.TestCase):
|
||||
"Tests for :class:`django_downloadviews.views.object.ObjectDownloadView`."
|
||||
def test_get_file_ok(self):
|
||||
"ObjectDownloadView.get_file() returns ``file`` field by default."
|
||||
view = setup_view(views.ObjectDownloadView(), 'fake request')
|
||||
view.object = mock.Mock(spec=['file'])
|
||||
view.get_file()
|
||||
|
||||
def test_get_file_wrong_field(self):
|
||||
"""ObjectDownloadView.get_file() raises AttributeError if field does
|
||||
not exist.
|
||||
|
||||
``AttributeError`` is expected because this is a configuration error,
|
||||
i.e. it is related to Python code.
|
||||
|
||||
"""
|
||||
view = setup_view(views.ObjectDownloadView(file_field='other_field'),
|
||||
'fake request')
|
||||
view.object = mock.Mock(spec=['file'])
|
||||
with self.assertRaises(AttributeError):
|
||||
view.get_file()
|
||||
|
||||
def test_get_file_empty_field(self):
|
||||
"""ObjectDownloadView.get_file() raises FileNotFound if field does not
|
||||
exist."""
|
||||
view = setup_view(views.ObjectDownloadView(file_field='other_field'),
|
||||
'fake request')
|
||||
view.object = mock.Mock()
|
||||
view.object.other_field = None
|
||||
with self.assertRaises(exceptions.FileNotFound):
|
||||
view.get_file()
|
||||
|
||||
|
||||
class VirtualDownloadViewTestCase(unittest.TestCase):
|
||||
"""Test suite around
|
||||
|
|
|
|||
38
tox.ini
38
tox.ini
|
|
@ -1,40 +1,45 @@
|
|||
[tox]
|
||||
envlist = py27,py33,flake8,sphinx,readme
|
||||
envlist = py{27,33,34}-django{15,16,17,18}, flake8, sphinx, readme
|
||||
|
||||
[testenv]
|
||||
basepython =
|
||||
py27: python2.7
|
||||
py33: python3.3
|
||||
py34: python3.4
|
||||
deps =
|
||||
nose
|
||||
rednose
|
||||
coverage
|
||||
django15: Django>=1.5,<1.6
|
||||
django16: Django>=1.6,<1.7
|
||||
django17: Django>=1.7,<1.8
|
||||
django18: Django>=1.8,<1.9
|
||||
nose
|
||||
py27: mock
|
||||
commands =
|
||||
pip install ./
|
||||
pip install -e demo/
|
||||
demo test --nose-verbosity=2 -c etc/nose/base.cfg -c etc/nose/django_downloadview.cfg django_downloadview
|
||||
demo test --nose-verbosity=2 demoproject
|
||||
rm .coverage
|
||||
pip install -e .
|
||||
pip install -e demo
|
||||
demo test --cover-package=django_downloadview --cover-package=demoproject {posargs: tests demoproject}
|
||||
coverage erase
|
||||
pip freeze
|
||||
whitelist_externals =
|
||||
rm
|
||||
|
||||
[testenv:flake8]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
flake8
|
||||
commands =
|
||||
flake8 django_downloadview/
|
||||
flake8 demo/demoproject/
|
||||
flake8 demo django_downloadview tests
|
||||
|
||||
[testenv:sphinx]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
nose
|
||||
rednose
|
||||
Sphinx
|
||||
commands =
|
||||
pip install ./
|
||||
make --directory=docs SPHINXBUILD="sphinx-build -W" clean html doctest
|
||||
pip install -e .
|
||||
make --directory=docs SPHINXOPTS='-W' clean {posargs:html doctest linkcheck}
|
||||
whitelist_externals =
|
||||
make
|
||||
|
||||
[testenv:readme]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
docutils
|
||||
pygments
|
||||
|
|
@ -47,6 +52,7 @@ whitelist_externals =
|
|||
|
||||
[testenv:release]
|
||||
deps =
|
||||
wheel
|
||||
zest.releaser
|
||||
commands =
|
||||
fullrelease
|
||||
|
|
|
|||
Loading…
Reference in a new issue