Code

Ticket #15270: 15270.1.diff

File 15270.1.diff, 17.4 KB (added by jezdez, 3 years ago)

moves back the code, adds static url pattern helper that is used by staticfiles now, too. probably needs more doc updates in the howto section

Line 
1diff --git a/django/conf/urls/static.py b/django/conf/urls/static.py
2new file mode 100644
3index 0000000..07a67cc
4--- /dev/null
5+++ b/django/conf/urls/static.py
6@@ -0,0 +1,21 @@
7+import re
8+from django.conf import settings
9+from django.conf.urls.defaults import patterns, url
10+from django.core.exceptions import ImproperlyConfigured
11+
12+def static(prefix, view='django.views.static.serve', **kwargs):
13+    """
14+    Helper function to return a URL pattern for serving files in debug mode.
15+
16+    urlpatterns += static(
17+        prefix=settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
18+    """
19+    if not settings.DEBUG:
20+        return []
21+    elif not prefix:
22+        raise ImproperlyConfigured("Empty static prefix not permitted")
23+    elif '://' in prefix:
24+        raise ImproperlyConfigured("URL '%s' not allowed as static prefix" % prefix)
25+    return patterns('',
26+        url(r'^%s(?P<path>.*)$' % re.escape(prefix.lstrip('/')), view, **kwargs),
27+    )
28diff --git a/django/contrib/staticfiles/urls.py b/django/contrib/staticfiles/urls.py
29index aa4ab45..04062c1 100644
30--- a/django/contrib/staticfiles/urls.py
31+++ b/django/contrib/staticfiles/urls.py
32@@ -1,28 +1,16 @@
33-import re
34 from django.conf import settings
35-from django.conf.urls.defaults import patterns, url, include
36-from django.core.exceptions import ImproperlyConfigured
37+from django.conf.urls.static import static
38 
39 urlpatterns = []
40 
41-# only serve non-fqdn URLs
42-if settings.DEBUG:
43-    urlpatterns += patterns('',
44-        url(r'^(?P<path>.*)$', 'django.contrib.staticfiles.views.serve'),
45-    )
46-
47 def staticfiles_urlpatterns(prefix=None):
48     """
49     Helper function to return a URL pattern for serving static files.
50     """
51-    if not settings.DEBUG:
52-        return []
53     if prefix is None:
54         prefix = settings.STATIC_URL
55-    if not prefix or '://' in prefix:
56-        raise ImproperlyConfigured(
57-            "The prefix for the 'staticfiles_urlpatterns' helper is invalid.")
58-    if prefix.startswith("/"):
59-        prefix = prefix[1:]
60-    return patterns('',
61-        url(r'^%s' % re.escape(prefix), include(urlpatterns)),)
62+    return static(prefix, view='django.contrib.staticfiles.views.serve')
63+
64+# Only append if urlpatterns are empty
65+if settings.DEBUG and not urlpatterns:
66+    urlpatterns += staticfiles_urlpatterns()
67diff --git a/django/contrib/staticfiles/views.py b/django/contrib/staticfiles/views.py
68index f5a6ec3..4d2a50d 100644
69--- a/django/contrib/staticfiles/views.py
70+++ b/django/contrib/staticfiles/views.py
71@@ -3,24 +3,16 @@ Views and functions for serving static files. These are only to be used during
72 development, and SHOULD NOT be used in a production setting.
73 
74 """
75-import mimetypes
76 import os
77-import posixpath
78-import re
79-import stat
80-import urllib
81-from email.Utils import parsedate_tz, mktime_tz
82 
83 from django.conf import settings
84 from django.core.exceptions import ImproperlyConfigured
85-from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified
86-from django.template import loader, Template, Context, TemplateDoesNotExist
87-from django.utils.http import http_date
88+from django.http import Http404
89+from django.views.static import serve as static_serve
90 
91-from django.contrib.staticfiles import finders, utils
92+from django.contrib.staticfiles import finders
93 
94-
95-def serve(request, path, document_root=None, show_indexes=False, insecure=False):
96+def serve(request, path, document_root=None, insecure=False, **kwargs):
97     """
98     Serve static files below a given point in the directory structure or
99     from locations inferred from the static files finders.
100@@ -42,124 +34,13 @@ def serve(request, path, document_root=None, show_indexes=False, insecure=False)
101     a template called ``static/directory_index.html``.
102     """
103     if not settings.DEBUG and not insecure:
104-        raise ImproperlyConfigured("The view to serve static files can only "
105-                                   "be used if the DEBUG setting is True or "
106-                                   "the --insecure option of 'runserver' is "
107-                                   "used")
108+        raise ImproperlyConfigured("The staticfiles view can only be used in "
109+                                   "debug mode or if the the --insecure "
110+                                   "option of 'runserver' is used")
111     if not document_root:
112         path = os.path.normpath(path)
113         absolute_path = finders.find(path)
114         if not absolute_path:
115             raise Http404('"%s" could not be found' % path)
116         document_root, path = os.path.split(absolute_path)
117-    # Clean up given path to only allow serving files below document_root.
118-    path = posixpath.normpath(urllib.unquote(path))
119-    path = path.lstrip('/')
120-    newpath = ''
121-    for part in path.split('/'):
122-        if not part:
123-            # Strip empty path components.
124-            continue
125-        drive, part = os.path.splitdrive(part)
126-        head, part = os.path.split(part)
127-        if part in (os.curdir, os.pardir):
128-            # Strip '.' and '..' in path.
129-            continue
130-        newpath = os.path.join(newpath, part).replace('\\', '/')
131-    if newpath and path != newpath:
132-        return HttpResponseRedirect(newpath)
133-    fullpath = os.path.join(document_root, newpath)
134-    if os.path.isdir(fullpath):
135-        if show_indexes:
136-            return directory_index(newpath, fullpath)
137-        raise Http404("Directory indexes are not allowed here.")
138-    if not os.path.exists(fullpath):
139-        raise Http404('"%s" does not exist' % fullpath)
140-    # Respect the If-Modified-Since header.
141-    statobj = os.stat(fullpath)
142-    mimetype, encoding = mimetypes.guess_type(fullpath)
143-    mimetype = mimetype or 'application/octet-stream'
144-    if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
145-                              statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]):
146-        return HttpResponseNotModified(mimetype=mimetype)
147-    contents = open(fullpath, 'rb').read()
148-    response = HttpResponse(contents, mimetype=mimetype)
149-    response["Last-Modified"] = http_date(statobj[stat.ST_MTIME])
150-    response["Content-Length"] = len(contents)
151-    if encoding:
152-        response["Content-Encoding"] = encoding
153-    return response
154-
155-
156-DEFAULT_DIRECTORY_INDEX_TEMPLATE = """
157-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
158-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
159-  <head>
160-    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
161-    <meta http-equiv="Content-Language" content="en-us" />
162-    <meta name="robots" content="NONE,NOARCHIVE" />
163-    <title>Index of {{ directory }}</title>
164-  </head>
165-  <body>
166-    <h1>Index of {{ directory }}</h1>
167-    <ul>
168-      {% ifnotequal directory "/" %}
169-      <li><a href="../">../</a></li>
170-      {% endifnotequal %}
171-      {% for f in file_list %}
172-      <li><a href="{{ f|urlencode }}">{{ f }}</a></li>
173-      {% endfor %}
174-    </ul>
175-  </body>
176-</html>
177-"""
178-
179-def directory_index(path, fullpath):
180-    try:
181-        t = loader.select_template(['static/directory_index.html',
182-                'static/directory_index'])
183-    except TemplateDoesNotExist:
184-        t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template')
185-    files = []
186-    for f in os.listdir(fullpath):
187-        if not f.startswith('.'):
188-            if os.path.isdir(os.path.join(fullpath, f)):
189-                f += '/'
190-            files.append(f)
191-    c = Context({
192-        'directory' : path + '/',
193-        'file_list' : files,
194-    })
195-    return HttpResponse(t.render(c))
196-
197-def was_modified_since(header=None, mtime=0, size=0):
198-    """
199-    Was something modified since the user last downloaded it?
200-
201-    header
202-      This is the value of the If-Modified-Since header.  If this is None,
203-      I'll just return True.
204-
205-    mtime
206-      This is the modification time of the item we're talking about.
207-
208-    size
209-      This is the size of the item we're talking about.
210-    """
211-    try:
212-        if header is None:
213-            raise ValueError
214-        matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header,
215-                           re.IGNORECASE)
216-        header_date = parsedate_tz(matches.group(1))
217-        if header_date is None:
218-            raise ValueError
219-        header_mtime = mktime_tz(header_date)
220-        header_len = matches.group(3)
221-        if header_len and int(header_len) != size:
222-            raise ValueError
223-        if mtime > header_mtime:
224-            raise ValueError
225-    except (AttributeError, ValueError, OverflowError):
226-        return True
227-    return False
228+    return static_serve(request, path, document_root=document_root, **kwargs)
229diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py
230index 7772a0b..34c76f4 100644
231--- a/django/core/servers/basehttp.py
232+++ b/django/core/servers/basehttp.py
233@@ -18,8 +18,9 @@ import warnings
234 from django.core.management.color import color_style
235 from django.utils.http import http_date
236 from django.utils._os import safe_join
237+from django.views.static import serve
238 
239-from django.contrib.staticfiles import handlers, views as static
240+from django.contrib.staticfiles import handlers
241 
242 __version__ = "0.1"
243 __all__ = ['WSGIServer','WSGIRequestHandler']
244@@ -677,8 +678,7 @@ class AdminMediaHandler(handlers.StaticFilesHandler):
245 
246     def serve(self, request):
247         document_root, path = os.path.split(self.file_path(request.path))
248-        return static.serve(request, path,
249-            document_root=document_root, insecure=True)
250+        return serve(request, path, document_root=document_root)
251 
252     def _should_handle(self, path):
253         """
254diff --git a/django/views/static.py b/django/views/static.py
255index 2ce886f..9b34312 100644
256--- a/django/views/static.py
257+++ b/django/views/static.py
258@@ -9,7 +9,6 @@ import posixpath
259 import re
260 import stat
261 import urllib
262-import warnings
263 from email.Utils import parsedate_tz, mktime_tz
264 
265 from django.template import loader
266@@ -17,11 +16,8 @@ from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpRespons
267 from django.template import Template, Context, TemplateDoesNotExist
268 from django.utils.http import http_date
269 
270-from django.contrib.staticfiles.views import (directory_index,
271-    was_modified_since, serve as staticfiles_serve)
272 
273-
274-def serve(request, path, document_root=None, show_indexes=False, insecure=False):
275+def serve(request, path, document_root=None, show_indexes=False):
276     """
277     Serve static files below a given point in the directory structure.
278 
279@@ -35,7 +31,114 @@ def serve(request, path, document_root=None, show_indexes=False, insecure=False)
280     but if you'd like to override it, you can create a template called
281     ``static/directory_index.html``.
282     """
283-    warnings.warn("The view at `django.views.static.serve` is deprecated; "
284-                  "use the path `django.contrib.staticfiles.views.serve` "
285-                  "instead.", PendingDeprecationWarning)
286-    return staticfiles_serve(request, path, document_root, show_indexes, insecure)
287+    # Clean up given path to only allow serving files below document_root.
288+    path = posixpath.normpath(urllib.unquote(path))
289+    path = path.lstrip('/')
290+    newpath = ''
291+    for part in path.split('/'):
292+        if not part:
293+            # Strip empty path components.
294+            continue
295+        drive, part = os.path.splitdrive(part)
296+        head, part = os.path.split(part)
297+        if part in (os.curdir, os.pardir):
298+            # Strip '.' and '..' in path.
299+            continue
300+        newpath = os.path.join(newpath, part).replace('\\', '/')
301+    if newpath and path != newpath:
302+        return HttpResponseRedirect(newpath)
303+    fullpath = os.path.join(document_root, newpath)
304+    if os.path.isdir(fullpath):
305+        if show_indexes:
306+            return directory_index(newpath, fullpath)
307+        raise Http404("Directory indexes are not allowed here.")
308+    if not os.path.exists(fullpath):
309+        raise Http404('"%s" does not exist' % fullpath)
310+    # Respect the If-Modified-Since header.
311+    statobj = os.stat(fullpath)
312+    mimetype, encoding = mimetypes.guess_type(fullpath)
313+    mimetype = mimetype or 'application/octet-stream'
314+    if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
315+                              statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]):
316+        return HttpResponseNotModified(mimetype=mimetype)
317+    contents = open(fullpath, 'rb').read()
318+    response = HttpResponse(contents, mimetype=mimetype)
319+    response["Last-Modified"] = http_date(statobj[stat.ST_MTIME])
320+    response["Content-Length"] = len(contents)
321+    if encoding:
322+        response["Content-Encoding"] = encoding
323+    return response
324+
325+
326+DEFAULT_DIRECTORY_INDEX_TEMPLATE = """
327+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
328+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
329+  <head>
330+    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
331+    <meta http-equiv="Content-Language" content="en-us" />
332+    <meta name="robots" content="NONE,NOARCHIVE" />
333+    <title>Index of {{ directory }}</title>
334+  </head>
335+  <body>
336+    <h1>Index of {{ directory }}</h1>
337+    <ul>
338+      {% ifnotequal directory "/" %}
339+      <li><a href="../">../</a></li>
340+      {% endifnotequal %}
341+      {% for f in file_list %}
342+      <li><a href="{{ f|urlencode }}">{{ f }}</a></li>
343+      {% endfor %}
344+    </ul>
345+  </body>
346+</html>
347+"""
348+
349+def directory_index(path, fullpath):
350+    try:
351+        t = loader.select_template(['static/directory_index.html',
352+                'static/directory_index'])
353+    except TemplateDoesNotExist:
354+        t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template')
355+    files = []
356+    for f in os.listdir(fullpath):
357+        if not f.startswith('.'):
358+            if os.path.isdir(os.path.join(fullpath, f)):
359+                f += '/'
360+            files.append(f)
361+    c = Context({
362+        'directory' : path + '/',
363+        'file_list' : files,
364+    })
365+    return HttpResponse(t.render(c))
366+
367+def was_modified_since(header=None, mtime=0, size=0):
368+    """
369+    Was something modified since the user last downloaded it?
370+
371+    header
372+      This is the value of the If-Modified-Since header.  If this is None,
373+      I'll just return True.
374+
375+    mtime
376+      This is the modification time of the item we're talking about.
377+
378+    size
379+      This is the size of the item we're talking about.
380+    """
381+    try:
382+        if header is None:
383+            raise ValueError
384+        matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header,
385+                           re.IGNORECASE)
386+        header_date = parsedate_tz(matches.group(1))
387+        if header_date is None:
388+            raise ValueError
389+        header_mtime = mktime_tz(header_date)
390+        header_len = matches.group(3)
391+        if header_len and int(header_len) != size:
392+            raise ValueError
393+        if mtime > header_mtime:
394+            raise ValueError
395+    except (AttributeError, ValueError, OverflowError):
396+        return True
397+    return False
398diff --git a/docs/howto/static-files.txt b/docs/howto/static-files.txt
399index 94e04bb..b68004d 100644
400--- a/docs/howto/static-files.txt
401+++ b/docs/howto/static-files.txt
402@@ -109,10 +109,9 @@ the framework see :doc:`the staticfiles reference </ref/contrib/staticfiles>`.
403    :setting:`MEDIA_URL` different from your :setting:`STATIC_ROOT` and
404    :setting:`STATIC_URL`. You will need to arrange for serving of files in
405    :setting:`MEDIA_ROOT` yourself; ``staticfiles`` does not deal with
406-   user-uploaded files at all. You can, however, use ``staticfiles``'
407-   :func:`~django.contrib.staticfiles.views.serve` view for serving
408-   :setting:`MEDIA_ROOT` in development; see
409-   :ref:`staticfiles-serve-other-directories`.
410+   user-uploaded files at all. You can, however, use
411+   :func:`~django.views.static.serve` view for serving :setting:`MEDIA_ROOT`
412+   in development; see :ref:`staticfiles-serve-other-directories`.
413 
414 .. _staticfiles-in-templates:
415 
416diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt
417index dd26fe5..9a9ee0b 100644
418--- a/docs/ref/contrib/staticfiles.txt
419+++ b/docs/ref/contrib/staticfiles.txt
420@@ -343,5 +343,13 @@ by appending something like this to your URLconf::
421                {'document_root': settings.MEDIA_ROOT}),
422        )
423 
424+Luckily Django also ships with a small URL helper function that shrinks
425+that down a bit::
426+
427+    from django.conf import settings
428+    from django.conf.urls.static import static
429+
430+    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
431+
432 This snippet assumes you've also set your :setting:`MEDIA_URL` (in development)
433 to ``/media/``.
434diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py
435index d524254..0ceb11b 100644
436--- a/tests/regressiontests/staticfiles_tests/tests.py
437+++ b/tests/regressiontests/staticfiles_tests/tests.py
438@@ -293,9 +293,8 @@ class TestServeDisabled(TestServeStatic):
439         settings.DEBUG = False
440 
441     def test_disabled_serving(self):
442-        self.assertRaisesRegexp(ImproperlyConfigured, 'The view to serve '
443-            'static files can only be used if the DEBUG setting is True',
444-            self._response, 'test.txt')
445+        self.assertRaisesRegexp(ImproperlyConfigured, 'The staticfiles view '
446+            'can only be used in debug mode ', self._response, 'test.txt')
447 
448 
449 class TestServeStaticWithDefaultURL(TestServeStatic, TestDefaults):