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
This commit is contained in:
Jannis Leidel 2011-02-14 01:42:26 +00:00
parent 6b1191b1a2
commit a26034ffbf
8 changed files with 223 additions and 211 deletions

View File

@ -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<path>.*)$' % re.escape(prefix.lstrip('/')), view, **kwargs),
)

View File

@ -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<path>.*)$', '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()

View File

@ -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<path>.*)$', '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 = """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="en-us" />
<meta name="robots" content="NONE,NOARCHIVE" />
<title>Index of {{ directory }}</title>
</head>
<body>
<h1>Index of {{ directory }}</h1>
<ul>
{% ifnotequal directory "/" %}
<li><a href="../">../</a></li>
{% endifnotequal %}
{% for f in file_list %}
<li><a href="{{ f|urlencode }}">{{ f }}</a></li>
{% endfor %}
</ul>
</body>
</html>
"""
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)

View File

@ -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):
"""

View File

@ -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 = """
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="en-us" />
<meta name="robots" content="NONE,NOARCHIVE" />
<title>Index of {{ directory }}</title>
</head>
<body>
<h1>Index of {{ directory }}</h1>
<ul>
{% ifnotequal directory "/" %}
<li><a href="../">../</a></li>
{% endifnotequal %}
{% for f in file_list %}
<li><a href="{{ f|urlencode }}">{{ f }}</a></li>
{% endfor %}
</ul>
</body>
</html>
"""
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

View File

@ -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 </ref/contrib/staticfiles>`.
: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<path>.*)$', '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

View File

@ -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<path>.*)$', 'serve',
{'document_root': settings.MEDIA_ROOT}),
)
This snippet assumes you've also set your :setting:`MEDIA_URL` (in development)
to ``/media/``.

View File

@ -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):