Skip to content

Commit f62145a

Browse files
committed
Merge pull request jazzband#548 from chipx86/static-view
Add a view for collecting static files before serving them.
2 parents b072c5a + 00c580a commit f62145a

File tree

5 files changed

+184
-2
lines changed

5 files changed

+184
-2
lines changed

pipeline/collector.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from django.contrib.staticfiles import finders
88
from django.contrib.staticfiles.storage import staticfiles_storage
9+
from django.utils import six
910

1011
from pipeline.finders import PipelineFinder
1112

@@ -26,7 +27,7 @@ def clear(self, path=""):
2627
for d in dirs:
2728
self.clear(os.path.join(path, d))
2829

29-
def collect(self, request=None):
30+
def collect(self, request=None, files=[]):
3031
if self.request and self.request is request:
3132
return
3233
self.request = request
@@ -41,10 +42,17 @@ def collect(self, request=None):
4142
prefixed_path = os.path.join(storage.prefix, path)
4243
else:
4344
prefixed_path = path
44-
if prefixed_path not in found_files:
45+
46+
if (prefixed_path not in found_files and
47+
(not files or prefixed_path in files)):
4548
found_files[prefixed_path] = (storage, path)
4649
self.copy_file(path, prefixed_path, storage)
4750

51+
if files and len(files) == len(found_files):
52+
break
53+
54+
return six.iterkeys(found_files)
55+
4856
def copy_file(self, path, prefixed_path, source_storage):
4957
# Delete the target file if needed or break
5058
if not self.delete_file(path, prefixed_path, source_storage):

pipeline/views.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from __future__ import unicode_literals
2+
3+
from django.conf import settings as django_settings
4+
from django.core.exceptions import ImproperlyConfigured
5+
from django.views.static import serve
6+
7+
from .collector import default_collector
8+
from .conf import settings
9+
10+
11+
def serve_static(request, path, insecure=False, **kwargs):
12+
"""Collect and serve static files.
13+
14+
This view serves up static files, much like Django's
15+
:py:func:`~django.views.static.serve` view, with the addition that it
16+
collects static files first (if enabled). This allows images, fonts, and
17+
other assets to be served up without first loading a page using the
18+
``{% javascript %}`` or ``{% stylesheet %}`` template tags.
19+
20+
You can use this view by adding the following to any :file:`urls.py`::
21+
22+
urlpatterns += static('static/', view='pipeline.views.serve_static')
23+
"""
24+
# Follow the same logic Django uses for determining access to the
25+
# static-serving view.
26+
if not django_settings.DEBUG and not insecure:
27+
raise ImproperlyConfigured("The staticfiles view can only be used in "
28+
"debug mode or if the --insecure "
29+
"option of 'runserver' is used")
30+
31+
if not settings.PIPELINE_ENABLED and settings.PIPELINE_COLLECTOR_ENABLED:
32+
# Collect only the requested file, in order to serve the result as
33+
# fast as possible. This won't interfere with the template tags in any
34+
# way, as those will still cause Django to collect all media.
35+
default_collector.collect(request, files=[path])
36+
37+
return serve(request, path, document_root=django_settings.STATIC_ROOT,
38+
**kwargs)

tests/tests/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
os.environ.setdefault('NUMBER_OF_PROCESSORS', '1')
88

99

10+
from .test_collector import *
1011
from .test_compiler import *
1112
from .test_compressor import *
1213
from .test_template import *
@@ -15,3 +16,4 @@
1516
from .test_packager import *
1617
from .test_storage import *
1718
from .test_utils import *
19+
from .test_views import *

tests/tests/test_collector.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from __future__ import unicode_literals
2+
3+
import os
4+
5+
from django.contrib.staticfiles import finders
6+
from django.test import TestCase
7+
8+
from pipeline.collector import default_collector
9+
from pipeline.finders import PipelineFinder
10+
11+
12+
class CollectorTest(TestCase):
13+
def tearDown(self):
14+
super(CollectorTest, self).tearDown()
15+
16+
default_collector.clear()
17+
18+
def test_collect(self):
19+
self.assertEqual(
20+
set(default_collector.collect()),
21+
set(self._get_collectable_files()))
22+
23+
def test_collect_with_files(self):
24+
self.assertEqual(
25+
set(default_collector.collect(files=[
26+
'pipeline/js/first.js',
27+
'pipeline/js/second.js',
28+
])),
29+
set([
30+
'pipeline/js/first.js',
31+
'pipeline/js/second.js',
32+
]))
33+
34+
def _get_collectable_files(self):
35+
for finder in finders.get_finders():
36+
if not isinstance(finder, PipelineFinder):
37+
for path, storage in finder.list(['CVS', '.*', '*~']):
38+
if getattr(storage, 'prefix', None):
39+
yield os.path.join(storage.prefix, path)
40+
else:
41+
yield path

tests/tests/test_views.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from __future__ import unicode_literals
2+
3+
from django.contrib.staticfiles.storage import staticfiles_storage
4+
from django.core.exceptions import ImproperlyConfigured
5+
from django.http import Http404
6+
from django.test import RequestFactory, TestCase
7+
from django.test.utils import override_settings
8+
9+
from pipeline.collector import default_collector
10+
from pipeline.views import serve_static
11+
from tests.utils import pipeline_settings
12+
13+
14+
@override_settings(DEBUG=True)
15+
@pipeline_settings(PIPELINE_COLLECTOR_ENABLED=True, PIPELINE_ENABLED=False)
16+
class ServeStaticViewsTest(TestCase):
17+
def setUp(self):
18+
super(ServeStaticViewsTest, self).setUp()
19+
20+
self.filename = 'pipeline/js/first.js'
21+
self.storage = staticfiles_storage
22+
self.request = RequestFactory().get('/static/%s' % self.filename)
23+
24+
default_collector.clear()
25+
26+
def tearDown(self):
27+
super(ServeStaticViewsTest, self).tearDown()
28+
29+
default_collector.clear()
30+
staticfiles_storage._setup()
31+
32+
def test_found(self):
33+
self._test_found()
34+
35+
def test_not_found(self):
36+
self._test_not_found('missing-file')
37+
38+
@override_settings(DEBUG=False)
39+
def test_debug_false(self):
40+
with self.assertRaises(ImproperlyConfigured):
41+
serve_static(self.request, self.filename)
42+
43+
self.assertFalse(self.storage.exists(self.filename))
44+
45+
@override_settings(DEBUG=False)
46+
def test_debug_false_and_insecure(self):
47+
self._test_found(insecure=True)
48+
49+
@pipeline_settings(PIPELINE_ENABLED=True)
50+
def test_pipeline_enabled_and_found(self):
51+
self._write_content()
52+
self._test_found()
53+
54+
@pipeline_settings(PIPELINE_ENABLED=True)
55+
def test_pipeline_enabled_and_not_found(self):
56+
self._test_not_found(self.filename)
57+
58+
@pipeline_settings(PIPELINE_COLLECTOR_ENABLED=False)
59+
def test_collector_disabled_and_found(self):
60+
self._write_content()
61+
self._test_found()
62+
63+
@pipeline_settings(PIPELINE_COLLECTOR_ENABLED=False)
64+
def test_collector_disabled_and_not_found(self):
65+
self._test_not_found(self.filename)
66+
67+
def _write_content(self, content='abc123'):
68+
"""Write sample content to the test static file."""
69+
with self.storage.open(self.filename, 'w') as f:
70+
f.write(content)
71+
72+
def _test_found(self, **kwargs):
73+
"""Test that a file can be found and contains the correct content."""
74+
response = serve_static(self.request, self.filename, **kwargs)
75+
self.assertEqual(response.status_code, 200)
76+
self.assertTrue(self.storage.exists(self.filename))
77+
78+
if hasattr(response, 'streaming_content'):
79+
content = b''.join(response.streaming_content)
80+
else:
81+
content = response.content
82+
83+
with self.storage.open(self.filename) as f:
84+
self.assertEqual(f.read(), content)
85+
86+
def _test_not_found(self, filename):
87+
"""Test that a file could not be found."""
88+
self.assertFalse(self.storage.exists(filename))
89+
90+
with self.assertRaises(Http404):
91+
serve_static(self.request, filename)
92+
93+
self.assertFalse(self.storage.exists(filename))

0 commit comments

Comments
 (0)