From a26034ffbf8951276b79ccb298423bc809246637 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Mon, 14 Feb 2011 01:42:26 +0000 Subject: [PATCH] Fixes #15270 -- Moved back the serve view to django.views.static due to dependency conflicts with the contrib app staticfiles (reverts parts of r14293). Added a helper function that generates URL patterns for serving static and media files during development. Thanks to Carl for reviewing the patch. git-svn-id: http://code.djangoproject.com/svn/django/trunk@15530 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/urls/static.py | 26 +++ django/contrib/staticfiles/urls.py | 24 +-- django/contrib/staticfiles/views.py | 153 ++---------------- django/core/servers/basehttp.py | 6 +- django/views/static.py | 121 ++++++++++++-- docs/howto/static-files.txt | 71 ++++++-- docs/ref/contrib/staticfiles.txt | 28 ---- .../staticfiles_tests/tests.py | 5 +- 8 files changed, 223 insertions(+), 211 deletions(-) create mode 100644 django/conf/urls/static.py diff --git a/django/conf/urls/static.py b/django/conf/urls/static.py new file mode 100644 index 0000000000..f90b8b5cab --- /dev/null +++ b/django/conf/urls/static.py @@ -0,0 +1,26 @@ +import re +from django.conf import settings +from django.conf.urls.defaults import patterns, url +from django.core.exceptions import ImproperlyConfigured + +def static(prefix, view='django.views.static.serve', **kwargs): + """ + Helper function to return a URL pattern for serving files in debug mode. + + from django.conf import settings + from django.conf.urls.static import static + + urlpatterns = patterns('', + # ... the rest of your URLconf goes here ... + ) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + + """ + if not settings.DEBUG: + return [] + elif not prefix: + raise ImproperlyConfigured("Empty static prefix not permitted") + elif '://' in prefix: + raise ImproperlyConfigured("URL '%s' not allowed as static prefix" % prefix) + return patterns('', + url(r'^%s(?P.*)$' % re.escape(prefix.lstrip('/')), view, **kwargs), + ) diff --git a/django/contrib/staticfiles/urls.py b/django/contrib/staticfiles/urls.py index aa4ab459d1..04062c1162 100644 --- a/django/contrib/staticfiles/urls.py +++ b/django/contrib/staticfiles/urls.py @@ -1,28 +1,16 @@ -import re from django.conf import settings -from django.conf.urls.defaults import patterns, url, include -from django.core.exceptions import ImproperlyConfigured +from django.conf.urls.static import static urlpatterns = [] -# only serve non-fqdn URLs -if settings.DEBUG: - urlpatterns += patterns('', - url(r'^(?P.*)$', 'django.contrib.staticfiles.views.serve'), - ) - def staticfiles_urlpatterns(prefix=None): """ Helper function to return a URL pattern for serving static files. """ - if not settings.DEBUG: - return [] if prefix is None: prefix = settings.STATIC_URL - if not prefix or '://' in prefix: - raise ImproperlyConfigured( - "The prefix for the 'staticfiles_urlpatterns' helper is invalid.") - if prefix.startswith("/"): - prefix = prefix[1:] - return patterns('', - url(r'^%s' % re.escape(prefix), include(urlpatterns)),) + return static(prefix, view='django.contrib.staticfiles.views.serve') + +# Only append if urlpatterns are empty +if settings.DEBUG and not urlpatterns: + urlpatterns += staticfiles_urlpatterns() diff --git a/django/contrib/staticfiles/views.py b/django/contrib/staticfiles/views.py index f5a6ec3dae..123a01a557 100644 --- a/django/contrib/staticfiles/views.py +++ b/django/contrib/staticfiles/views.py @@ -3,27 +3,21 @@ Views and functions for serving static files. These are only to be used during development, and SHOULD NOT be used in a production setting. """ -import mimetypes import os import posixpath -import re -import stat import urllib -from email.Utils import parsedate_tz, mktime_tz from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified -from django.template import loader, Template, Context, TemplateDoesNotExist -from django.utils.http import http_date +from django.http import Http404 +from django.views import static -from django.contrib.staticfiles import finders, utils +from django.contrib.staticfiles import finders - -def serve(request, path, document_root=None, show_indexes=False, insecure=False): +def serve(request, path, document_root=None, insecure=False, **kwargs): """ Serve static files below a given point in the directory structure or - from locations inferred from the static files finders. + from locations inferred from the staticfiles finders. To use, put a URL pattern such as:: @@ -31,135 +25,14 @@ def serve(request, path, document_root=None, show_indexes=False, insecure=False) in your URLconf. - If you provide the ``document_root`` parameter, the file won't be looked - up with the staticfiles finders, but in the given filesystem path, e.g.:: - - (r'^(?P.*)$', 'django.contrib.staticfiles.views.serve', {'document_root' : '/path/to/my/files/'}) - - You may also set ``show_indexes`` to ``True`` if you'd like to serve a - basic index of the directory. This index view will use the - template hardcoded below, but if you'd like to override it, you can create - a template called ``static/directory_index.html``. + It automatically falls back to django.views.static """ if not settings.DEBUG and not insecure: - raise ImproperlyConfigured("The view to serve static files can only " - "be used if the DEBUG setting is True or " - "the --insecure option of 'runserver' is " - "used") - if not document_root: - path = os.path.normpath(path) - absolute_path = finders.find(path) - if not absolute_path: - raise Http404('"%s" could not be found' % path) + raise ImproperlyConfigured("The staticfiles view can only be used in " + "debug mode or if the the --insecure " + "option of 'runserver' is used") + normalized_path = posixpath.normpath(urllib.unquote(path)).lstrip('/') + absolute_path = finders.find(normalized_path) + if absolute_path: document_root, path = os.path.split(absolute_path) - # Clean up given path to only allow serving files below document_root. - path = posixpath.normpath(urllib.unquote(path)) - path = path.lstrip('/') - newpath = '' - for part in path.split('/'): - if not part: - # Strip empty path components. - continue - drive, part = os.path.splitdrive(part) - head, part = os.path.split(part) - if part in (os.curdir, os.pardir): - # Strip '.' and '..' in path. - continue - newpath = os.path.join(newpath, part).replace('\\', '/') - if newpath and path != newpath: - return HttpResponseRedirect(newpath) - fullpath = os.path.join(document_root, newpath) - if os.path.isdir(fullpath): - if show_indexes: - return directory_index(newpath, fullpath) - raise Http404("Directory indexes are not allowed here.") - if not os.path.exists(fullpath): - raise Http404('"%s" does not exist' % fullpath) - # Respect the If-Modified-Since header. - statobj = os.stat(fullpath) - mimetype, encoding = mimetypes.guess_type(fullpath) - mimetype = mimetype or 'application/octet-stream' - if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), - statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]): - return HttpResponseNotModified(mimetype=mimetype) - contents = open(fullpath, 'rb').read() - response = HttpResponse(contents, mimetype=mimetype) - response["Last-Modified"] = http_date(statobj[stat.ST_MTIME]) - response["Content-Length"] = len(contents) - if encoding: - response["Content-Encoding"] = encoding - return response - - -DEFAULT_DIRECTORY_INDEX_TEMPLATE = """ - - - - - - - Index of {{ directory }} - - -

Index of {{ directory }}

-
    - {% ifnotequal directory "/" %} -
  • ../
  • - {% endifnotequal %} - {% for f in file_list %} -
  • {{ f }}
  • - {% endfor %} -
- - -""" - -def directory_index(path, fullpath): - try: - t = loader.select_template(['static/directory_index.html', - 'static/directory_index']) - except TemplateDoesNotExist: - t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template') - files = [] - for f in os.listdir(fullpath): - if not f.startswith('.'): - if os.path.isdir(os.path.join(fullpath, f)): - f += '/' - files.append(f) - c = Context({ - 'directory' : path + '/', - 'file_list' : files, - }) - return HttpResponse(t.render(c)) - -def was_modified_since(header=None, mtime=0, size=0): - """ - Was something modified since the user last downloaded it? - - header - This is the value of the If-Modified-Since header. If this is None, - I'll just return True. - - mtime - This is the modification time of the item we're talking about. - - size - This is the size of the item we're talking about. - """ - try: - if header is None: - raise ValueError - matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header, - re.IGNORECASE) - header_date = parsedate_tz(matches.group(1)) - if header_date is None: - raise ValueError - header_mtime = mktime_tz(header_date) - header_len = matches.group(3) - if header_len and int(header_len) != size: - raise ValueError - if mtime > header_mtime: - raise ValueError - except (AttributeError, ValueError, OverflowError): - return True - return False + return static.serve(request, path, document_root=document_root, **kwargs) diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 7772a0b5f8..4e179dc7c0 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -18,8 +18,9 @@ import warnings from django.core.management.color import color_style from django.utils.http import http_date from django.utils._os import safe_join +from django.views import static -from django.contrib.staticfiles import handlers, views as static +from django.contrib.staticfiles import handlers __version__ = "0.1" __all__ = ['WSGIServer','WSGIRequestHandler'] @@ -677,8 +678,7 @@ class AdminMediaHandler(handlers.StaticFilesHandler): def serve(self, request): document_root, path = os.path.split(self.file_path(request.path)) - return static.serve(request, path, - document_root=document_root, insecure=True) + return static.serve(request, path, document_root=document_root) def _should_handle(self, path): """ diff --git a/django/views/static.py b/django/views/static.py index 2ce886f7ac..da1158d9d2 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -9,7 +9,6 @@ import posixpath import re import stat import urllib -import warnings from email.Utils import parsedate_tz, mktime_tz from django.template import loader @@ -17,11 +16,7 @@ from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpRespons from django.template import Template, Context, TemplateDoesNotExist from django.utils.http import http_date -from django.contrib.staticfiles.views import (directory_index, - was_modified_since, serve as staticfiles_serve) - - -def serve(request, path, document_root=None, show_indexes=False, insecure=False): +def serve(request, path, document_root=None, show_indexes=False): """ Serve static files below a given point in the directory structure. @@ -35,7 +30,113 @@ def serve(request, path, document_root=None, show_indexes=False, insecure=False) but if you'd like to override it, you can create a template called ``static/directory_index.html``. """ - warnings.warn("The view at `django.views.static.serve` is deprecated; " - "use the path `django.contrib.staticfiles.views.serve` " - "instead.", PendingDeprecationWarning) - return staticfiles_serve(request, path, document_root, show_indexes, insecure) + path = posixpath.normpath(urllib.unquote(path)) + path = path.lstrip('/') + newpath = '' + for part in path.split('/'): + if not part: + # Strip empty path components. + continue + drive, part = os.path.splitdrive(part) + head, part = os.path.split(part) + if part in (os.curdir, os.pardir): + # Strip '.' and '..' in path. + continue + newpath = os.path.join(newpath, part).replace('\\', '/') + if newpath and path != newpath: + return HttpResponseRedirect(newpath) + fullpath = os.path.join(document_root, newpath) + if os.path.isdir(fullpath): + if show_indexes: + return directory_index(newpath, fullpath) + raise Http404("Directory indexes are not allowed here.") + if not os.path.exists(fullpath): + raise Http404('"%s" does not exist' % fullpath) + # Respect the If-Modified-Since header. + statobj = os.stat(fullpath) + mimetype, encoding = mimetypes.guess_type(fullpath) + mimetype = mimetype or 'application/octet-stream' + if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), + statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]): + return HttpResponseNotModified(mimetype=mimetype) + contents = open(fullpath, 'rb').read() + response = HttpResponse(contents, mimetype=mimetype) + response["Last-Modified"] = http_date(statobj[stat.ST_MTIME]) + response["Content-Length"] = len(contents) + if encoding: + response["Content-Encoding"] = encoding + return response + + +DEFAULT_DIRECTORY_INDEX_TEMPLATE = """ + + + + + + + Index of {{ directory }} + + +

Index of {{ directory }}

+
    + {% ifnotequal directory "/" %} +
  • ../
  • + {% endifnotequal %} + {% for f in file_list %} +
  • {{ f }}
  • + {% endfor %} +
+ + +""" + +def directory_index(path, fullpath): + try: + t = loader.select_template(['static/directory_index.html', + 'static/directory_index']) + except TemplateDoesNotExist: + t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template') + files = [] + for f in os.listdir(fullpath): + if not f.startswith('.'): + if os.path.isdir(os.path.join(fullpath, f)): + f += '/' + files.append(f) + c = Context({ + 'directory' : path + '/', + 'file_list' : files, + }) + return HttpResponse(t.render(c)) + +def was_modified_since(header=None, mtime=0, size=0): + """ + Was something modified since the user last downloaded it? + + header + This is the value of the If-Modified-Since header. If this is None, + I'll just return True. + + mtime + This is the modification time of the item we're talking about. + + size + This is the size of the item we're talking about. + """ + try: + if header is None: + raise ValueError + matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header, + re.IGNORECASE) + header_date = parsedate_tz(matches.group(1)) + if header_date is None: + raise ValueError + header_mtime = mktime_tz(header_date) + header_len = matches.group(3) + if header_len and int(header_len) != size: + raise ValueError + if mtime > header_mtime: + raise ValueError + except (AttributeError, ValueError, OverflowError): + return True + return False diff --git a/docs/howto/static-files.txt b/docs/howto/static-files.txt index 94e04bb738..3faeaeba41 100644 --- a/docs/howto/static-files.txt +++ b/docs/howto/static-files.txt @@ -2,8 +2,6 @@ Managing static files ===================== -.. currentmodule:: django.contrib.staticfiles - .. versionadded:: 1.3 Django developers mostly concern themselves with the dynamic parts of web @@ -109,10 +107,9 @@ the framework see :doc:`the staticfiles reference `. :setting:`MEDIA_URL` different from your :setting:`STATIC_ROOT` and :setting:`STATIC_URL`. You will need to arrange for serving of files in :setting:`MEDIA_ROOT` yourself; ``staticfiles`` does not deal with - user-uploaded files at all. You can, however, use ``staticfiles``' - :func:`~django.contrib.staticfiles.views.serve` view for serving - :setting:`MEDIA_ROOT` in development; see - :ref:`staticfiles-serve-other-directories`. + user-uploaded files at all. You can, however, use + :func:`~django.views.static.serve` view for serving :setting:`MEDIA_ROOT` + in development; see :ref:`staticfiles-other-directories`. .. _staticfiles-in-templates: @@ -241,8 +238,64 @@ files in app directories. :setting:`STATIC_URL` setting can't be empty or a full URL, such as ``http://static.example.com/``. -For a few more details, including an alternate method of enabling this view, -see :ref:`staticfiles-development-view`. +For a few more details on how the ``staticfiles`` can be used during +development, see :ref:`staticfiles-development-view`. + +.. _staticfiles-other-directories: + +Serving other directories +------------------------- + +.. currentmodule:: django.views.static +.. function:: serve(request, path, document_root, show_indexes=False) + +There may be files other than your project's static assets that, for +convenience, you'd like to have Django serve for you in local development. +The :func:`~django.views.static.serve` view can be used to serve any directory +you give it. (Again, this view is **not** hardened for production +use, and should be used only as a development aid; you should serve these files +in production using a real front-end webserver). + +The most likely example is user-uploaded content in :setting:`MEDIA_ROOT`. +``staticfiles`` is intended for static assets and has no built-in handling +for user-uploaded files, but you can have Django serve your +:setting:`MEDIA_ROOT` by appending something like this to your URLconf:: + + from django.conf import settings + + # ... the rest of your URLconf goes here ... + + if settings.DEBUG: + urlpatterns += patterns('', + url(r'^media/(?P.*)$', 'django.views.static', { + 'document_root': settings.MEDIA_ROOT, + }), + ) + +Note, the snippet assumes your :setting:`MEDIA_URL` has a value of +``'/media/'``. This will call the :func:`~django.views.static.serve` view, +passing in the path from the URLconf and the (required) ``document_root`` +parameter. + +.. currentmodule:: django.conf.urls.static +.. function:: static(prefix, view='django.views.static.serve', **kwargs) + +Since it can become a bit cumbersome to define this URL pattern, Django +ships with a small URL helper function +:func:`~django.conf.urls.static.static` that taks as parameters the prefix +such as :setting:`MEDIA_URL` and a dotted path to a view, such as +``'django.views.static.serve'``. Any other function parameter will be +transparently passed to the view. + +An example for serving :setting:`MEDIA_URL` (``'/media/'``) during +development:: + + from django.conf import settings + from django.conf.urls.static import static + + urlpatterns = patterns('', + # ... the rest of your URLconf goes here ... + ) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) .. _staticfiles-production: @@ -395,7 +448,7 @@ Upgrading from ``django-staticfiles`` ===================================== ``django.contrib.staticfiles`` began its life as `django-staticfiles`_. If -you're upgrading from `django-staticfiles`_ < ``1.0``` (e.g. ``0.3.4``) to +you're upgrading from `django-staticfiles`_ older than 1.0 (e.g. 0.3.4) to ``django.contrib.staticfiles``, you'll need to make a few changes: * Application files should now live in a ``static`` directory in each app diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index dd26fe535b..b06620d085 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -317,31 +317,3 @@ already defined pattern list. Use it like this:: This helper function will only work if :setting:`DEBUG` is ``True`` and your :setting:`STATIC_URL` setting is neither empty nor a full URL such as ``http://static.example.com/``. - -.. _staticfiles-serve-other-directories: - -Serving other directories -""""""""""""""""""""""""" - -There may be files other than your project's static assets that, for -convenience, you'd like to have Django serve for you in local development. The -:func:`~django.contrib.staticfiles.views.serve` view can be used to serve any -directory you give it. (Again, this view is **not** hardened for production -use, and should be used only as a development aid; you should serve these files -in production using a real front-end webserver). - -The most likely example is user-uploaded content in :setting:`MEDIA_ROOT`. -``staticfiles`` is intended for static assets and has no built-in handling for -user-uploaded files, but you can have Django serve your :setting:`MEDIA_ROOT` -by appending something like this to your URLconf:: - - from django.conf import settings - - if settings.DEBUG: - urlpatterns += patterns('django.contrib.staticfiles.views', - url(r'^media/(?P.*)$', 'serve', - {'document_root': settings.MEDIA_ROOT}), - ) - -This snippet assumes you've also set your :setting:`MEDIA_URL` (in development) -to ``/media/``. diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py index d524254f3c..0ceb11bafd 100644 --- a/tests/regressiontests/staticfiles_tests/tests.py +++ b/tests/regressiontests/staticfiles_tests/tests.py @@ -293,9 +293,8 @@ class TestServeDisabled(TestServeStatic): settings.DEBUG = False def test_disabled_serving(self): - self.assertRaisesRegexp(ImproperlyConfigured, 'The view to serve ' - 'static files can only be used if the DEBUG setting is True', - self._response, 'test.txt') + self.assertRaisesRegexp(ImproperlyConfigured, 'The staticfiles view ' + 'can only be used in debug mode ', self._response, 'test.txt') class TestServeStaticWithDefaultURL(TestServeStatic, TestDefaults):