From 536728bc0a95f08c83fe10ffa950a1a97d025009 Mon Sep 17 00:00:00 2001 From: Benoit Bryon Date: Mon, 27 Aug 2012 15:57:47 +0200 Subject: [PATCH] Introduced DownloadView. Introduced demo project, used to develop and test the library. Work in progress. --- Makefile | 3 +- buildout.cfg | 7 +- demo/README | 5 + demo/demoproject/__init__.py | 0 demo/demoproject/download/__init__.py | 1 + .../download/fixtures/hello-world.txt | 1 + demo/demoproject/download/models.py | 0 demo/demoproject/download/tests.py | 31 ++++ demo/demoproject/download/urls.py | 8 + demo/demoproject/download/views.py | 11 ++ demo/demoproject/manage.py | 13 ++ demo/demoproject/settings.py | 158 ++++++++++++++++++ demo/demoproject/urls.py | 6 + demo/demoproject/wsgi.py | 27 +++ demo/setup.py | 45 +++++ django_downloadview/__init__.py | 2 + django_downloadview/views.py | 33 ++++ 17 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 demo/README create mode 100644 demo/demoproject/__init__.py create mode 100644 demo/demoproject/download/__init__.py create mode 100644 demo/demoproject/download/fixtures/hello-world.txt create mode 100644 demo/demoproject/download/models.py create mode 100644 demo/demoproject/download/tests.py create mode 100644 demo/demoproject/download/urls.py create mode 100644 demo/demoproject/download/views.py create mode 100755 demo/demoproject/manage.py create mode 100755 demo/demoproject/settings.py create mode 100755 demo/demoproject/urls.py create mode 100755 demo/demoproject/wsgi.py create mode 100644 demo/setup.py diff --git a/Makefile b/Makefile index c31ba0a..d38b84c 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,8 @@ maintainer-clean: distclean test: - bin/nosetests --config=etc/nose.cfg + #bin/nosetests --config=etc/nose.cfg + bin/demo test download documentation: diff --git a/buildout.cfg b/buildout.cfg index f7e4c20..cd6e088 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -18,12 +18,15 @@ eggs-directory = lib/buildout/eggs installed = lib/buildout/.installed.cfg parts-directory = lib/buildout/parts # Development. -develop = ${buildout:directory}/ +develop = + ${buildout:directory}/ + ${buildout:directory}/demo/ [django-downloadview] recipe = z3c.recipe.scripts eggs = - django-downloadview + # We install the demo project, which depends on django-downloadview. + django-downloadview-demo [testing] recipe = z3c.recipe.scripts diff --git a/demo/README b/demo/README new file mode 100644 index 0000000..589b43e --- /dev/null +++ b/demo/README @@ -0,0 +1,5 @@ +############################ +Demo for Django-DownloadView +############################ + +This is a demo project to illustrate (and test) Django-DownloadView usage. diff --git a/demo/demoproject/__init__.py b/demo/demoproject/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/demoproject/download/__init__.py b/demo/demoproject/download/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/demo/demoproject/download/__init__.py @@ -0,0 +1 @@ + diff --git a/demo/demoproject/download/fixtures/hello-world.txt b/demo/demoproject/download/fixtures/hello-world.txt new file mode 100644 index 0000000..cd08755 --- /dev/null +++ b/demo/demoproject/download/fixtures/hello-world.txt @@ -0,0 +1 @@ +Hello world! diff --git a/demo/demoproject/download/models.py b/demo/demoproject/download/models.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/demoproject/download/tests.py b/demo/demoproject/download/tests.py new file mode 100644 index 0000000..a5a3ccc --- /dev/null +++ b/demo/demoproject/download/tests.py @@ -0,0 +1,31 @@ +"""Test suite for django-downloadview.""" +from os import listdir +from os.path import abspath, dirname, join + +from django.core.urlresolvers import reverse +from django.test import TestCase + + +app_dir = dirname(abspath(__file__)) +fixtures_dir = join(app_dir, 'fixtures') + + +class DownloadViewTestCase(TestCase): + """Test generic DownloadView.""" + def setUp(self): + """Common setup.""" + super(DownloadViewTestCase, self).setUp() + self.download_hello_world_url = reverse('download_hello_world') + self.files = {} + for f in listdir(fixtures_dir): + self.files[f] = abspath(join(fixtures_dir, f)) + + def test_download_hello_world(self): + """Download_hello_world view returns hello-world.txt as attachement.""" + response = self.client.get(self.download_hello_world_url) + self.assertEquals(response.status_code, 200) + self.assertEquals(response['Content-Type'], 'application/octet-stream') + self.assertEquals(response['Content-Disposition'], + 'attachment; filename=hello-world.txt') + self.assertEqual(open(self.files['hello-world.txt']).read(), + response.content) diff --git a/demo/demoproject/download/urls.py b/demo/demoproject/download/urls.py new file mode 100644 index 0000000..0a80f62 --- /dev/null +++ b/demo/demoproject/download/urls.py @@ -0,0 +1,8 @@ +"""URLconf for tests.""" +from django.conf.urls import patterns, include, url + + +urlpatterns = patterns('demoproject.download.views', + url(r'^download/hello-world\.txt$', 'download_hello_world', + name='download_hello_world'), +) diff --git a/demo/demoproject/download/views.py b/demo/demoproject/download/views.py new file mode 100644 index 0000000..bf62e98 --- /dev/null +++ b/demo/demoproject/download/views.py @@ -0,0 +1,11 @@ +from os.path import abspath, dirname, join + +from django_downloadview import DownloadView + + +app_dir = dirname(abspath(__file__)) +fixtures_dir = join(app_dir, 'fixtures') +hello_world_file = join(fixtures_dir, 'hello-world.txt') + + +download_hello_world = DownloadView.as_view(file=hello_world_file) diff --git a/demo/demoproject/manage.py b/demo/demoproject/manage.py new file mode 100755 index 0000000..e785df9 --- /dev/null +++ b/demo/demoproject/manage.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +import os +import sys + +from django.core.management import execute_from_command_line + +def main(): + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "%s.settings" % __package__) + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/demo/demoproject/settings.py b/demo/demoproject/settings.py new file mode 100755 index 0000000..7d0dbe1 --- /dev/null +++ b/demo/demoproject/settings.py @@ -0,0 +1,158 @@ +# Django settings for Django-DownloadView demo project. +from os.path import abspath, dirname, join + +demoproject_dir = dirname(abspath(__file__)) +demo_dir = dirname(demoproject_dir) +root_dir = dirname(demo_dir) +data_dir = join(root_dir, 'var') + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': join(data_dir, 'db.sqlite'), # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# In a Windows environment this must be set to your system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale. +USE_L10N = True + +# If you set this to False, Django will not use timezone-aware datetimes. +USE_TZ = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '123456789' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + # Uncomment the next line for simple clickjacking protection: + # 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'demoproject.urls' + +# Python dotted path to the WSGI application used by Django's runserver. +WSGI_APPLICATION = 'demoproject.wsgi.application' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # Uncomment the next line to enable the admin: + # 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', + 'demoproject.download', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error when DEBUG=False. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} diff --git a/demo/demoproject/urls.py b/demo/demoproject/urls.py new file mode 100755 index 0000000..401ab5f --- /dev/null +++ b/demo/demoproject/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls import patterns, include, url + + +urlpatterns = patterns('', + url(r'^download/', include('demoproject.download.urls')), +) diff --git a/demo/demoproject/wsgi.py b/demo/demoproject/wsgi.py new file mode 100755 index 0000000..f83c09d --- /dev/null +++ b/demo/demoproject/wsgi.py @@ -0,0 +1,27 @@ +"""WSGI config for Django-DownloadView demo project. + +This module contains the WSGI application used by Django's development server +and any production WSGI deployments. It should expose a module-level variable +named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover +this application via the ``WSGI_APPLICATION`` setting. + +Usually you will have the standard Django WSGI application here, but it also +might make sense to replace the whole Django WSGI application with a custom one +that later delegates to the Django one. For example, you could introduce WSGI +middleware here, or combine a Django application with an application of another +framework. + +""" +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demoproject.settings") + +# This application object is used by any WSGI server configured to use this +# file. This includes Django's development server, if the WSGI_APPLICATION +# setting points here. +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + +# Apply WSGI middleware here. +# from helloworld.wsgi import HelloWorldApplication +# application = HelloWorldApplication(application) diff --git a/demo/setup.py b/demo/setup.py new file mode 100644 index 0000000..87fb839 --- /dev/null +++ b/demo/setup.py @@ -0,0 +1,45 @@ +# coding=utf-8 +"""Python packaging.""" +import os +from setuptools import setup + + +def read_relative_file(filename): + """Returns contents of the given file, which path is supposed relative + to this module.""" + with open(os.path.join(os.path.dirname(__file__), filename)) as f: + return f.read() + + +NAME = 'django-downloadview-demo' +README = read_relative_file('README') +VERSION = '0.1' +PACKAGES = ['demoproject'] +REQUIRES = ['django-downloadview'] + + +setup(name=NAME, + version=VERSION, + description='Demo project for Django-DownloadView.', + long_description=README, + classifiers=['Development Status :: 1 - Planning', + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 2.6', + 'Framework :: Django', + ], + keywords='class-based view, generic view, download', + author='Benoit Bryon', + author_email='benoit@marmelune.net', + url='https://github.com/benoitbryon/%s' % NAME, + license='BSD', + packages=PACKAGES, + include_package_data=True, + zip_safe=False, + install_requires=REQUIRES, + entry_points={ + 'console_scripts': [ + 'demo = demoproject.manage:main', + ] + }, + ) diff --git a/django_downloadview/__init__.py b/django_downloadview/__init__.py index 6642980..2472765 100644 --- a/django_downloadview/__init__.py +++ b/django_downloadview/__init__.py @@ -1,4 +1,6 @@ """django-downloadview provides generic download views for Django.""" +from django_downloadview.views import DownloadView + #: Implement :pep:`396` __version__ = '0.1' diff --git a/django_downloadview/views.py b/django_downloadview/views.py index e69de29..8ecaf34 100644 --- a/django_downloadview/views.py +++ b/django_downloadview/views.py @@ -0,0 +1,33 @@ +from os.path import abspath, basename +import shutil + +from django.http import HttpResponse +from django.views.generic.base import View + + +class DownloadMixin(object): + file = None + response_class = HttpResponse + + def get_mime_type(self, file): + """Return mime-type of file.""" + return 'application/octet-stream' + + def render_to_response(self, file, **response_kwargs): + """Returns a response with a file as attachment.""" + mime_type = self.get_mime_type(file) + absolute_filename = abspath(file) + filename = basename(file) + response = self.response_class(mimetype=mime_type) + # Open file as read binary. + with open(absolute_filename, 'rb') as f: + shutil.copyfileobj(f, response) + # Do not call fsock.close() as HttpResponse needs it open + # Garbage collector will close it + response['Content-Disposition'] = 'attachment; filename=%s' % filename + return response + + +class DownloadView(DownloadMixin, View): + def get(self, request, *args, **kwargs): + return self.render_to_response(self.file)