Browse files

Fixed #15252 -- Added static template tag and CachedStaticFilesStorag…

…e to staticfiles contrib app.

Many thanks to Florian Apolloner and Jacob Kaplan-Moss for reviewing and eagle eyeing.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16594 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent e9a909e commit 1d32bdd3c9586ff10d0799264105850fa7e3f512 @jezdez jezdez committed Aug 11, 2011
Showing with 643 additions and 145 deletions.
  1. +2 −1 django/contrib/admin/helpers.py
  2. +7 −6 django/contrib/admin/options.py
  3. +1 −1 django/contrib/admin/templates/admin/auth/user/change_password.html
  4. +1 −1 django/contrib/admin/templates/admin/base.html
  5. +1 −1 django/contrib/admin/templates/admin/change_form.html
  6. +1 −1 django/contrib/admin/templates/admin/change_list.html
  7. +1 −1 django/contrib/admin/templates/admin/change_list_results.html
  8. +1 −1 django/contrib/admin/templates/admin/edit_inline/stacked.html
  9. +1 −1 django/contrib/admin/templates/admin/edit_inline/tabular.html
  10. +1 −1 django/contrib/admin/templates/admin/index.html
  11. +1 −1 django/contrib/admin/templates/admin/login.html
  12. +1 −1 django/contrib/admin/templates/admin/search_form.html
  13. +1 −1 django/contrib/admin/templatetags/admin_list.py
  14. +11 −0 django/contrib/admin/templatetags/admin_static.py
  15. +22 −12 django/contrib/admin/widgets.py
  16. +1 −1 django/contrib/staticfiles/finders.py
  17. +56 −28 django/contrib/staticfiles/management/commands/collectstatic.py
  18. +161 −4 django/contrib/staticfiles/storage.py
  19. 0 django/contrib/staticfiles/templatetags/__init__.py
  20. +13 −0 django/contrib/staticfiles/templatetags/staticfiles.py
  21. +9 −5 django/contrib/staticfiles/utils.py
  22. +20 −24 docs/howto/static-files.txt
  23. +140 −3 docs/ref/contrib/staticfiles.txt
  24. +14 −3 docs/ref/templates/builtins.txt
  25. +23 −0 docs/releases/1.4.txt
  26. +1 −0 tests/regressiontests/staticfiles_tests/project/documents/cached/absolute.css
  27. +1 −0 tests/regressiontests/staticfiles_tests/project/documents/cached/denorm.css
  28. 0 tests/regressiontests/staticfiles_tests/project/documents/cached/other.css
  29. +2 −0 tests/regressiontests/staticfiles_tests/project/documents/cached/relative.css
  30. +1 −0 tests/regressiontests/staticfiles_tests/project/documents/cached/styles.css
  31. +1 −0 tests/regressiontests/staticfiles_tests/project/documents/cached/url.css
  32. +1 −0 tests/regressiontests/staticfiles_tests/project/site_media/static/testfile.txt
  33. +146 −47 tests/regressiontests/staticfiles_tests/tests.py
View
3 django/contrib/admin/helpers.py
@@ -1,6 +1,7 @@
from django import forms
from django.contrib.admin.util import (flatten_fieldsets, lookup_field,
display_for_field, label_for_field, help_text_for_field)
+from django.contrib.admin.templatetags.admin_static import static
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields.related import ManyToManyRel
@@ -75,7 +76,7 @@ def __init__(self, form, name=None, readonly_fields=(), fields=(), classes=(),
def _media(self):
if 'collapse' in self.classes:
js = ['jquery.min.js', 'jquery.init.js', 'collapse.min.js']
- return forms.Media(js=['admin/js/%s' % url for url in js])
+ return forms.Media(js=[static('admin/js/%s' % url) for url in js])
return forms.Media()
media = property(_media)
View
13 django/contrib/admin/options.py
@@ -6,6 +6,7 @@
from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets, helpers
from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
+from django.contrib.admin.templatetags.admin_static import static
from django.contrib import messages
from django.views.decorators.csrf import csrf_protect
from django.core.exceptions import PermissionDenied, ValidationError
@@ -350,7 +351,8 @@ def urls(self):
return self.get_urls()
urls = property(urls)
- def _media(self):
+ @property
+ def media(self):
js = [
'core.js',
'admin/RelatedObjectLookups.js',
@@ -363,8 +365,7 @@ def _media(self):
js.extend(['urlify.js', 'prepopulate.min.js'])
if self.opts.get_ordered_objects():
js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.js'])
- return forms.Media(js=['admin/js/%s' % url for url in js])
- media = property(_media)
+ return forms.Media(js=[static('admin/js/%s' % url) for url in js])
def has_add_permission(self, request):
"""
@@ -1322,14 +1323,14 @@ def __init__(self, parent_model, admin_site):
if self.verbose_name_plural is None:
self.verbose_name_plural = self.model._meta.verbose_name_plural
- def _media(self):
+ @property
+ def media(self):
js = ['jquery.min.js', 'jquery.init.js', 'inlines.min.js']
if self.prepopulated_fields:
js.extend(['urlify.js', 'prepopulate.min.js'])
if self.filter_vertical or self.filter_horizontal:
js.extend(['SelectBox.js', 'SelectFilter2.js'])
- return forms.Media(js=['admin/js/%s' % url for url in js])
- media = property(_media)
+ return forms.Media(js=[static('admin/js/%s' % url) for url in js])
def get_formset(self, request, obj=None, **kwargs):
"""Returns a BaseInlineFormSet class for use in admin add/change views."""
View
2 django/contrib/admin/templates/admin/auth/user/change_password.html
@@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n static admin_modify %}
+{% load i18n admin_static admin_modify %}
{% load url from future %}
{% block extrahead %}{{ block.super }}
{% url 'admin:jsi18n' as jsi18nurl %}
View
2 django/contrib/admin/templates/admin/base.html
@@ -1,4 +1,4 @@
-{% load static %}{% load url from future %}<!DOCTYPE html>
+{% load admin_static %}{% load url from future %}<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
<head>
<title>{% block title %}{% endblock %}</title>
View
2 django/contrib/admin/templates/admin/change_form.html
@@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n static admin_modify %}
+{% load i18n admin_static admin_modify %}
{% load url from future %}
{% block extrahead %}{{ block.super }}
View
2 django/contrib/admin/templates/admin/change_list.html
@@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n static admin_list %}
+{% load i18n admin_static admin_list %}
{% load url from future %}
{% block extrastyle %}
{{ block.super }}
View
2 django/contrib/admin/templates/admin/change_list_results.html
@@ -1,4 +1,4 @@
-{% load i18n static %}
+{% load i18n admin_static %}
{% if result_hidden_fields %}
<div class="hiddenfields">{# DIV for HTML validation #}
{% for item in result_hidden_fields %}{{ item }}{% endfor %}
View
2 django/contrib/admin/templates/admin/edit_inline/stacked.html
@@ -1,4 +1,4 @@
-{% load i18n static %}
+{% load i18n admin_static %}
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
<h2>{{ inline_admin_formset.opts.verbose_name_plural|title }}</h2>
{{ inline_admin_formset.formset.management_form }}
View
2 django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -1,4 +1,4 @@
-{% load i18n static admin_modify %}
+{% load i18n admin_static admin_modify %}
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
{{ inline_admin_formset.formset.management_form }}
View
2 django/contrib/admin/templates/admin/index.html
@@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n static %}
+{% load i18n admin_static %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}" />{% endblock %}
View
2 django/contrib/admin/templates/admin/login.html
@@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n static %}
+{% load i18n admin_static %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/login.css" %}" />{% endblock %}
View
2 django/contrib/admin/templates/admin/search_form.html
@@ -1,4 +1,4 @@
-{% load i18n static %}
+{% load i18n admin_static %}
{% if cl.search_fields %}
<div id="toolbar"><form id="changelist-search" action="" method="get">
<div><!-- DIV needed for valid HTML -->
View
2 django/contrib/admin/templatetags/admin_list.py
@@ -3,9 +3,9 @@
from django.contrib.admin.util import lookup_field, display_for_field, label_for_field
from django.contrib.admin.views.main import (ALL_VAR, EMPTY_CHANGELIST_VALUE,
ORDER_VAR, PAGE_VAR, SEARCH_VAR)
+from django.contrib.admin.templatetags.admin_static import static
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
-from django.templatetags.static import static
from django.utils import formats
from django.utils.html import escape, conditional_escape
from django.utils.safestring import mark_safe
View
11 django/contrib/admin/templatetags/admin_static.py
@@ -0,0 +1,11 @@
+from django.conf import settings
+from django.template import Library
+
+register = Library()
+
+if 'django.contrib.staticfiles' in settings.INSTALLED_APPS:
+ from django.contrib.staticfiles.templatetags.staticfiles import static
+else:
+ from django.templatetags.static import static
+
+static = register.simple_tag(static)
View
34 django/contrib/admin/widgets.py
@@ -4,36 +4,40 @@
import copy
from django import forms
+from django.contrib.admin.templatetags.admin_static import static
from django.core.urlresolvers import reverse
from django.forms.widgets import RadioFieldRenderer
from django.forms.util import flatatt
-from django.templatetags.static import static
from django.utils.html import escape
from django.utils.text import Truncator
from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe
from django.utils.encoding import force_unicode
+
class FilteredSelectMultiple(forms.SelectMultiple):
"""
A SelectMultiple with a JavaScript filter interface.
Note that the resulting JavaScript assumes that the jsi18n
catalog has been loaded in the page
"""
- class Media:
- js = ["admin/js/%s" % path
- for path in ["core.js", "SelectBox.js", "SelectFilter2.js"]]
+ @property
+ def media(self):
+ js = ["core.js", "SelectBox.js", "SelectFilter2.js"]
+ return forms.Media(js=[static("admin/js/%s" % path) for path in js])
def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
self.verbose_name = verbose_name
self.is_stacked = is_stacked
super(FilteredSelectMultiple, self).__init__(attrs, choices)
def render(self, name, value, attrs=None, choices=()):
- if attrs is None: attrs = {}
+ if attrs is None:
+ attrs = {}
attrs['class'] = 'selectfilter'
- if self.is_stacked: attrs['class'] += 'stacked'
+ if self.is_stacked:
+ attrs['class'] += 'stacked'
output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)]
output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
# TODO: "id_" is hard-coded here. This should instead use the correct
@@ -43,15 +47,21 @@ def render(self, name, value, attrs=None, choices=()):
return mark_safe(u''.join(output))
class AdminDateWidget(forms.DateInput):
- class Media:
- js = ["admin/js/calendar.js", "admin/js/admin/DateTimeShortcuts.js"]
+
+ @property
+ def media(self):
+ js = ["calendar.js", "admin/DateTimeShortcuts.js"]
+ return forms.Media(js=[static("admin/js/%s" % path) for path in js])
def __init__(self, attrs={}, format=None):
super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'}, format=format)
class AdminTimeWidget(forms.TimeInput):
- class Media:
- js = ["admin/js/calendar.js", "admin/js/admin/DateTimeShortcuts.js"]
+
+ @property
+ def media(self):
+ js = ["calendar.js", "admin/DateTimeShortcuts.js"]
+ return forms.Media(js=[static("admin/js/%s" % path) for path in js])
def __init__(self, attrs={}, format=None):
super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'}, format=format)
@@ -232,9 +242,9 @@ def __deepcopy__(self, memo):
memo[id(self)] = obj
return obj
- def _media(self):
+ @property
+ def media(self):
return self.widget.media
- media = property(_media)
def render(self, name, value, *args, **kwargs):
rel_to = self.rel.to
View
2 django/contrib/staticfiles/finders.py
@@ -28,7 +28,7 @@ def find(self, path, all=False):
"""
raise NotImplementedError()
- def list(self, ignore_patterns=[]):
+ def list(self, ignore_patterns):
"""
Given an optional list of paths to ignore, this should return
a two item iterable consisting of the relative path and storage
View
84 django/contrib/staticfiles/management/commands/collectstatic.py
@@ -4,12 +4,11 @@
import sys
from optparse import make_option
-from django.conf import settings
-from django.core.files.storage import FileSystemStorage, get_storage_class
+from django.core.files.storage import FileSystemStorage
from django.core.management.base import CommandError, NoArgsCommand
from django.utils.encoding import smart_str, smart_unicode
-from django.contrib.staticfiles import finders
+from django.contrib.staticfiles import finders, storage
class Command(NoArgsCommand):
@@ -18,32 +17,39 @@ class Command(NoArgsCommand):
locations to the settings.STATIC_ROOT.
"""
option_list = NoArgsCommand.option_list + (
- make_option('--noinput', action='store_false', dest='interactive',
- default=True, help="Do NOT prompt the user for input of any kind."),
+ make_option('--noinput',
+ action='store_false', dest='interactive', default=True,
+ help="Do NOT prompt the user for input of any kind."),
+ make_option('--no-post-process',
+ action='store_false', dest='post_process', default=True,
+ help="Do NOT post process collected files."),
make_option('-i', '--ignore', action='append', default=[],
dest='ignore_patterns', metavar='PATTERN',
help="Ignore files or directories matching this glob-style "
"pattern. Use multiple times to ignore more."),
- make_option('-n', '--dry-run', action='store_true', dest='dry_run',
- default=False, help="Do everything except modify the filesystem."),
- make_option('-c', '--clear', action='store_true', dest='clear',
- default=False, help="Clear the existing files using the storage "
- "before trying to copy or link the original file."),
- make_option('-l', '--link', action='store_true', dest='link',
- default=False, help="Create a symbolic link to each file instead of copying."),
+ make_option('-n', '--dry-run',
+ action='store_true', dest='dry_run', default=False,
+ help="Do everything except modify the filesystem."),
+ make_option('-c', '--clear',
+ action='store_true', dest='clear', default=False,
+ help="Clear the existing files using the storage "
+ "before trying to copy or link the original file."),
+ make_option('-l', '--link',
+ action='store_true', dest='link', default=False,
+ help="Create a symbolic link to each file instead of copying."),
make_option('--no-default-ignore', action='store_false',
dest='use_default_ignore_patterns', default=True,
help="Don't ignore the common private glob-style patterns 'CVS', "
"'.*' and '*~'."),
)
- help = "Collect static files from apps and other locations in a single location."
+ help = "Collect static files in a single location."
def __init__(self, *args, **kwargs):
super(NoArgsCommand, self).__init__(*args, **kwargs)
self.copied_files = []
self.symlinked_files = []
self.unmodified_files = []
- self.storage = get_storage_class(settings.STATICFILES_STORAGE)()
+ self.storage = storage.staticfiles_storage
try:
self.storage.path('')
except NotImplementedError:
@@ -64,6 +70,7 @@ def handle_noargs(self, **options):
self.interactive = options['interactive']
self.symlink = options['link']
self.verbosity = int(options.get('verbosity', 1))
+ self.post_process = options['post_process']
if self.symlink:
if sys.platform == 'win32':
@@ -104,29 +111,46 @@ def handle_noargs(self, **options):
handler = {
True: self.link_file,
- False: self.copy_file
+ False: self.copy_file,
}[self.symlink]
+ found_files = []
for finder in finders.get_finders():
for path, storage in finder.list(self.ignore_patterns):
# Prefix the relative path if the source storage contains it
if getattr(storage, 'prefix', None):
prefixed_path = os.path.join(storage.prefix, path)
else:
prefixed_path = path
+ found_files.append(prefixed_path)
handler(path, prefixed_path, storage)
- actual_count = len(self.copied_files) + len(self.symlinked_files)
+ # Here we check if the storage backend has a post_process
+ # method and pass it the list of modified files.
+ if self.post_process and hasattr(self.storage, 'post_process'):
+ post_processed = self.storage.post_process(found_files, **options)
+ for path in post_processed:
+ self.log(u"Post-processed '%s'" % path, level=1)
+ else:
+ post_processed = []
+
+ modified_files = self.copied_files + self.symlinked_files
+ actual_count = len(modified_files)
unmodified_count = len(self.unmodified_files)
+
if self.verbosity >= 1:
- self.stdout.write(smart_str(u"\n%s static file%s %s %s%s.\n"
- % (actual_count,
- actual_count != 1 and 's' or '',
- self.symlink and 'symlinked' or 'copied',
- destination_path and "to '%s'"
- % destination_path or '',
- unmodified_count and ' (%s unmodified)'
- % unmodified_count or '')))
+ template = ("\n%(actual_count)s %(identifier)s %(action)s"
+ "%(destination)s%(unmodified)s.\n")
+ summary = template % {
+ 'actual_count': actual_count,
+ 'identifier': 'static file' + (actual_count > 1 and 's' or ''),
+ 'action': self.symlink and 'symlinked' or 'copied',
+ 'destination': (destination_path and " to '%s'"
+ % destination_path or ''),
+ 'unmodified': (self.unmodified_files and ', %s unmodified'
+ % unmodified_count or ''),
+ }
+ self.stdout.write(smart_str(summary))
def log(self, msg, level=2):
"""
@@ -146,7 +170,8 @@ def clear_dir(self, path):
for f in files:
fpath = os.path.join(path, f)
if self.dry_run:
- self.log(u"Pretending to delete '%s'" % smart_unicode(fpath), level=1)
+ self.log(u"Pretending to delete '%s'" %
+ smart_unicode(fpath), level=1)
else:
self.log(u"Deleting '%s'" % smart_unicode(fpath), level=1)
self.storage.delete(fpath)
@@ -159,7 +184,8 @@ def delete_file(self, path, prefixed_path, source_storage):
if self.storage.exists(prefixed_path):
try:
# When was the target file modified last time?
- target_last_modified = self.storage.modified_time(prefixed_path)
+ target_last_modified = \
+ self.storage.modified_time(prefixed_path)
except (OSError, NotImplementedError):
# The storage doesn't support ``modified_time`` or failed
pass
@@ -177,8 +203,10 @@ def delete_file(self, path, prefixed_path, source_storage):
full_path = None
# Skip the file if the source file is younger
if target_last_modified >= source_last_modified:
- if not ((self.symlink and full_path and not os.path.islink(full_path)) or
- (not self.symlink and full_path and os.path.islink(full_path))):
+ if not ((self.symlink and full_path
+ and not os.path.islink(full_path)) or
+ (not self.symlink and full_path
+ and os.path.islink(full_path))):
if prefixed_path not in self.unmodified_files:
self.unmodified_files.append(prefixed_path)
self.log(u"Skipping '%s' (not modified)" % path)
View
165 django/contrib/staticfiles/storage.py
@@ -1,10 +1,20 @@
+import hashlib
import os
+import posixpath
+import re
+
from django.conf import settings
+from django.core.cache import (get_cache, InvalidCacheBackendError,
+ cache as default_cache)
from django.core.exceptions import ImproperlyConfigured
-from django.core.files.storage import FileSystemStorage
+from django.core.files.base import ContentFile
+from django.core.files.storage import FileSystemStorage, get_storage_class
+from django.utils.encoding import force_unicode
+from django.utils.functional import LazyObject
from django.utils.importlib import import_module
+from django.utils.datastructures import SortedDict
-from django.contrib.staticfiles import utils
+from django.contrib.staticfiles.utils import check_settings, matches_patterns
class StaticFilesStorage(FileSystemStorage):
@@ -26,8 +36,148 @@ def __init__(self, location=None, base_url=None, *args, **kwargs):
if base_url is None:
raise ImproperlyConfigured("You're using the staticfiles app "
"without having set the STATIC_URL setting.")
- utils.check_settings()
- super(StaticFilesStorage, self).__init__(location, base_url, *args, **kwargs)
+ check_settings()
+ super(StaticFilesStorage, self).__init__(location, base_url,
+ *args, **kwargs)
+
+
+class CachedFilesMixin(object):
+ patterns = (
+ ("*.css", (
+ r"""(url\(['"]{0,1}\s*(.*?)["']{0,1}\))""",
+ r"""(@import\s*["']\s*(.*?)["'])""",
+ )),
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(CachedFilesMixin, self).__init__(*args, **kwargs)
+ try:
+ self.cache = get_cache('staticfiles')
+ except InvalidCacheBackendError:
+ # Use the default backend
+ self.cache = default_cache
+ self._patterns = SortedDict()
+ for extension, patterns in self.patterns:
+ for pattern in patterns:
+ compiled = re.compile(pattern)
+ self._patterns.setdefault(extension, []).append(compiled)
+
+ def hashed_name(self, name, content=None):
+ if content is None:
+ if not self.exists(name):
+ raise ValueError("The file '%s' could not be found with %r." %
+ (name, self))
+ try:
+ content = self.open(name)
+ except IOError:
+ # Handle directory paths
+ return name
+ path, filename = os.path.split(name)
+ root, ext = os.path.splitext(filename)
+ # Get the MD5 hash of the file
+ md5 = hashlib.md5()
+ for chunk in content.chunks():
+ md5.update(chunk)
+ md5sum = md5.hexdigest()[:12]
+ return os.path.join(path, u"%s.%s%s" % (root, md5sum, ext))
+
+ def cache_key(self, name):
+ return u'staticfiles:cache:%s' % name
+
+ def url(self, name, force=False):
+ """
+ Returns the real URL in DEBUG mode.
+ """
+ if settings.DEBUG and not force:
+ return super(CachedFilesMixin, self).url(name)
+ cache_key = self.cache_key(name)
+ hashed_name = self.cache.get(cache_key)
+ if hashed_name is None:
+ hashed_name = self.hashed_name(name)
+ return super(CachedFilesMixin, self).url(hashed_name)
+
+ def url_converter(self, name):
+ """
+ Returns the custom URL converter for the given file name.
+ """
+ def converter(matchobj):
+ """
+ Converts the matched URL depending on the parent level (`..`)
+ and returns the normalized and hashed URL using the url method
+ of the storage.
+ """
+ matched, url = matchobj.groups()
+ # Completely ignore http(s) prefixed URLs
+ if url.startswith(('http', 'https')):
+ return matched
+ name_parts = name.split('/')
+ # Using posix normpath here to remove duplicates
+ result = url_parts = posixpath.normpath(url).split('/')
+ level = url.count('..')
+ if level:
+ result = name_parts[:-level - 1] + url_parts[level:]
+ elif name_parts[:-1]:
+ result = name_parts[:-1] + url_parts[-1:]
+ joined_result = '/'.join(result)
+ hashed_url = self.url(joined_result, force=True)
+ # Return the hashed and normalized version to the file
+ return 'url("%s")' % hashed_url
+ return converter
+
+ def post_process(self, paths, dry_run=False, **options):
+ """
+ Post process the given list of files (called from collectstatic).
+ """
+ processed_files = []
+ # don't even dare to process the files if we're in dry run mode
+ if dry_run:
+ return processed_files
+
+ # delete cache of all handled paths
+ self.cache.delete_many([self.cache_key(path) for path in paths])
+
+ # only try processing the files we have patterns for
+ matches = lambda path: matches_patterns(path, self._patterns.keys())
+ processing_paths = [path for path in paths if matches(path)]
+
+ # then sort the files by the directory level
+ path_level = lambda name: len(name.split(os.sep))
+ for name in sorted(paths, key=path_level, reverse=True):
+
+ # first get a hashed name for the given file
+ hashed_name = self.hashed_name(name)
+
+ with self.open(name) as original_file:
+ # then get the original's file content
+ content = original_file.read()
+
+ # to apply each replacement pattern on the content
+ if name in processing_paths:
+ converter = self.url_converter(name)
+ for patterns in self._patterns.values():
+ for pattern in patterns:
+ content = pattern.sub(converter, content)
+
+ # then save the processed result
+ if self.exists(hashed_name):
+ self.delete(hashed_name)
+
+ saved_name = self._save(hashed_name, ContentFile(content))
+ hashed_name = force_unicode(saved_name.replace('\\', '/'))
+ processed_files.append(hashed_name)
+
+ # and then set the cache accordingly
+ self.cache.set(self.cache_key(name), hashed_name)
+
+ return processed_files
+
+
+class CachedStaticFilesStorage(CachedFilesMixin, StaticFilesStorage):
+ """
+ A static file system storage backend which also saves
+ hashed copies of the files it saves.
+ """
+ pass
class AppStaticStorage(FileSystemStorage):
@@ -47,3 +197,10 @@ def __init__(self, app, *args, **kwargs):
mod_path = os.path.dirname(mod.__file__)
location = os.path.join(mod_path, self.source_dir)
super(AppStaticStorage, self).__init__(location, *args, **kwargs)
+
+
+class ConfiguredStorage(LazyObject):
+ def _setup(self):
+ self._wrapped = get_storage_class(settings.STATICFILES_STORAGE)()
+
+staticfiles_storage = ConfiguredStorage()
View
0 django/contrib/staticfiles/templatetags/__init__.py
No changes.
View
13 django/contrib/staticfiles/templatetags/staticfiles.py
@@ -0,0 +1,13 @@
+from django import template
+from django.contrib.staticfiles.storage import staticfiles_storage
+
+register = template.Library()
+
+
+@register.simple_tag
+def static(path):
+ """
+ A template tag that returns the URL to a file
+ using staticfiles' storage backend
+ """
+ return staticfiles_storage.url(path)
View
14 django/contrib/staticfiles/utils.py
@@ -3,30 +3,34 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
-def is_ignored(path, ignore_patterns=[]):
+def matches_patterns(path, patterns=None):
"""
Return True or False depending on whether the ``path`` should be
ignored (if it matches any pattern in ``ignore_patterns``).
"""
- for pattern in ignore_patterns:
+ if patterns is None:
+ patterns = []
+ for pattern in patterns:
if fnmatch.fnmatchcase(path, pattern):
return True
return False
-def get_files(storage, ignore_patterns=[], location=''):
+def get_files(storage, ignore_patterns=None, location=''):
"""
Recursively walk the storage directories yielding the paths
of all files that should be copied.
"""
+ if ignore_patterns is None:
+ ignore_patterns = []
directories, files = storage.listdir(location)
for fn in files:
- if is_ignored(fn, ignore_patterns):
+ if matches_patterns(fn, ignore_patterns):
continue
if location:
fn = os.path.join(location, fn)
yield fn
for dir in directories:
- if is_ignored(dir, ignore_patterns):
+ if matches_patterns(dir, ignore_patterns):
continue
if location:
dir = os.path.join(location, dir)
View
44 docs/howto/static-files.txt
@@ -70,7 +70,7 @@ Basic usage
<img src="{{ STATIC_URL }}images/hi.jpg" />
- See :ref:`staticfiles-in-templates` for more details, including an
+ See :ref:`staticfiles-in-templates` for more details, **including** an
alternate method using a template tag.
Deploying static files in a nutshell
@@ -143,7 +143,7 @@ A far better way is to use the value of the :setting:`STATIC_URL` setting
directly in your templates. This means that a switch of static files servers
only requires changing that single value. Much better!
-``staticfiles`` includes two built-in ways of getting at this setting in your
+Django includes multiple built-in ways of using this setting in your
templates: a context processor and a template tag.
With a context processor
@@ -180,45 +180,41 @@ but in views written by hand you'll need to explicitly use ``RequestContext``
To see how that works, and to read more details, check out
:ref:`subclassing-context-requestcontext`.
+Another option is the :ttag:`get_static_prefix` template tag that is part of
+Django's core.
+
With a template tag
-------------------
-To easily link to static files Django ships with a :ttag:`static` template tag.
+The more powerful tool is the :ttag:`static<staticfiles-static>` template
+tag. It builds the URL for the given relative path by using the configured
+:setting:`STATICFILES_STORAGE` storage.
.. code-block:: html+django
- {% load static %}
+ {% load staticfiles %}
<img src="{% static "images/hi.jpg" %}" />
It is also able to consume standard context variables, e.g. assuming a
``user_stylesheet`` variable is passed to the template:
.. code-block:: html+django
- {% load static %}
+ {% load staticfiles %}
<link rel="stylesheet" href="{% static user_stylesheet %}" type="text/css" media="screen" />
-Another option is the :ttag:`get_static_prefix` template tag. You can use
-this if you're not using :class:`~django.template.RequestContext` (and
-therefore not relying on the ``django.core.context_processors.static``
-context processor), or if you need more control over exactly where and how
-:setting:`STATIC_URL` is injected into the template. Here's an example:
-
-.. code-block:: html+django
-
- {% load static %}
- <img src="{% get_static_prefix %}images/hi.jpg" />
-
-There's also a second form you can use to avoid extra processing if you need
-the value multiple times:
-
-.. code-block:: html+django
+.. note::
- {% load static %}
- {% get_static_prefix as STATIC_PREFIX %}
+ There is also a template tag named :ttag:`static` in Django's core set
+ of :ref:`built in template tags<ref-templates-builtins-tags>` which has
+ the same argument signature but only uses `urlparse.urljoin()`_ with the
+ :setting:`STATIC_URL` setting and the given path. This has the
+ disadvantage of not being able to easily switch the storage backend
+ without changing the templates, so in doubt use the ``staticfiles``
+ :ttag:`static<staticfiles-static>`
+ template tag.
- <img src="{{ STATIC_PREFIX }}images/hi.jpg" />
- <img src="{{ STATIC_PREFIX }}images/hi2.jpg" />
+.. _`urlparse.urljoin()`: http://docs.python.org/library/urlparse.html#urlparse.urljoin
.. _staticfiles-development:
View
143 docs/ref/contrib/staticfiles.txt
@@ -68,7 +68,9 @@ in a ``'downloads'`` subdirectory of :setting:`STATIC_ROOT`.
This would allow you to refer to the local file
``'/opt/webfiles/stats/polls_20101022.tar.gz'`` with
-``'/static/downloads/polls_20101022.tar.gz'`` in your templates, e.g.::
+``'/static/downloads/polls_20101022.tar.gz'`` in your templates, e.g.:
+
+.. code-block:: html+django
<a href="{{ STATIC_URL }}downloads/polls_20101022.tar.gz">
@@ -82,6 +84,11 @@ Default: ``'django.contrib.staticfiles.storage.StaticFilesStorage'``
The file storage engine to use when collecting static files with the
:djadmin:`collectstatic` management command.
+.. versionadded:: 1.4
+
+A ready-to-use instance of the storage backend defined in this setting
+can be found at ``django.contrib.staticfiles.storage.staticfiles_storage``.
+
For an example, see :ref:`staticfiles-from-cdn`.
.. setting:: STATICFILES_FINDERS
@@ -141,6 +148,16 @@ Files are searched by using the :setting:`enabled finders
:setting:`STATICFILES_DIRS` and in the ``'static'`` directory of apps
specified by the :setting:`INSTALLED_APPS` setting.
+.. versionadded:: 1.4
+
+The :djadmin:`collectstatic` management command calls the
+:meth:`~django.contrib.staticfiles.storage.StaticFilesStorage.post_process`
+method of the :setting:`STATICFILES_STORAGE` after each run and passes
+a list of paths that have been found by the management command. It also
+receives all command line options of :djadmin:`collectstatic`. This is used
+by the :class:`~django.contrib.staticfiles.storage.CachedStaticFilesStorage`
+by default.
+
Some commonly used options are:
.. django-admin-option:: --noinput
@@ -169,6 +186,13 @@ Some commonly used options are:
Create a symbolic link to each file instead of copying.
+.. django-admin-option:: --no-post-process
+.. versionadded:: 1.4
+
+ Don't call the
+ :meth:`~django.contrib.staticfiles.storage.StaticFilesStorage.post_process`
+ method of the configured :setting:`STATICFILES_STORAGE` storage backend.
+
.. django-admin-option:: --no-default-ignore
Don't ignore the common private glob-style patterns ``'CVS'``, ``'.*'``
@@ -237,7 +261,120 @@ Example usage::
django-admin.py runserver --insecure
-.. currentmodule:: None
+Storages
+========
+
+StaticFilesStorage
+------------------
+
+.. class:: storage.StaticFilesStorage
+
+ A subclass of the :class:`~django.core.files.storage.FileSystemStorage`
+ storage backend that uses the :setting:`STATIC_ROOT` setting as the base
+ file system location and the :setting:`STATIC_URL` setting respectively
+ as the base URL.
+
+ .. method:: post_process(paths, **options)
+
+ .. versionadded:: 1.4
+
+ This method is called by the :djadmin:`collectstatic` management command
+ after each run and gets passed the paths of found files, as well as the
+ command line options.
+
+ The :class:`~django.contrib.staticfiles.storage.CachedStaticFilesStorage`
+ uses this behind the scenes to replace the paths with their hashed
+ counterparts and update the cache appropriately.
+
+CachedStaticFilesStorage
+------------------------
+
+.. class:: storage.CachedStaticFilesStorage
+
+ .. versionadded:: 1.4
+
+ A subclass of the :class:`~django.contrib.staticfiles.storage.StaticFilesStorage`
+ storage backend which caches the files it saves by appending the MD5 hash
+ of the file's content to the filename. For example, the file
+ ``css/styles.css`` would also be saved as ``css/styles.55e7cbb9ba48.css``.
+
+ The purpose of this storage is to keep serving the old files in case some
+ pages still refer to those files, e.g. because they are cached by you or
+ a 3rd party proxy server. Additionally, it's very helpful if you want to
+ apply `far future Expires headers`_ to the deployed files to speed up the
+ load time for subsequent page visits.
+
+ The storage backend automatically replaces the paths found in the saved
+ files matching other saved files with the path of the cached copy (using
+ the :meth:`~django.contrib.staticfiles.storage.StaticFilesStorage.post_process`
+ method). The regular expressions used to find those paths
+ (``django.contrib.staticfiles.storage.CachedStaticFilesStorage.cached_patterns``)
+ by default cover the `@import`_ rule and `url()`_ statement of `Cascading
+ Style Sheets`_. For example, the ``'css/styles.css'`` file with the
+ content
+
+ .. code-block:: css+django
+
+ @import url("../admin/css/base.css");
+
+ would be replaced by calling the
+ :meth:`~django.core.files.storage.Storage.url`
+ method of the ``CachedStaticFilesStorage`` storage backend, ultimatively
+ saving a ``'css/styles.55e7cbb9ba48.css'`` file with the following
+ content:
+
+ .. code-block:: css+django
+
+ @import url("/static/admin/css/base.27e20196a850.css");
+
+ To enable the ``CachedStaticFilesStorage`` you have to make sure the
+ following requirements are met:
+
+ * the :setting:`STATICFILES_STORAGE` setting is set to
+ ``'django.contrib.staticfiles.storage.CachedStaticFilesStorage'``
+ * the :setting:`DEBUG` setting is set to ``False``
+ * you use the ``staticfiles`` :ttag:`static<staticfiles-static>` template
+ tag to refer to your static files in your templates
+ * you've collected all your static files by using the
+ :djadmin:`collectstatic` management command
+
+ Since creating the MD5 hash can be a performance burden to your website
+ during runtime, ``staticfiles`` will automatically try to cache the
+ hashed name for each file path using Django's :doc:`caching
+ framework</topics/cache>`. If you want to override certain options of the
+ cache backend the storage uses, simply specify a custom entry in the
+ :setting:`CACHES` setting named ``'staticfiles'``. It falls back to using
+ the ``'default'`` cache backend.
+
+.. _`far future Expires headers`: http://developer.yahoo.com/performance/rules.html#expires
+.. _`@import`: http://www.w3.org/TR/CSS2/cascade.html#at-import
+.. _`url()`: http://www.w3.org/TR/CSS2/syndata.html#uri
+.. _`Cascading Style Sheets`: http://www.w3.org/Style/CSS/
+
+.. currentmodule:: django.contrib.staticfiles.templatetags.staticfiles
+
+Template tags
+=============
+
+static
+------
+
+.. templatetag:: staticfiles-static
+
+.. versionadded:: 1.4
+
+Uses the configued :setting:`STATICFILES_STORAGE` storage to create the
+full URL for the given relative path, e.g.:
+
+.. code-block:: html+django
+
+ {% load static from staticfiles %}
+ <img src="{% static "css/base.css" %}" />
+
+The previous example is equal to calling the ``url`` method of an instance of
+:setting:`STATICFILES_STORAGE` with ``"css/base.css"``. This is especially
+useful when using a non-local storage backend to deploy files as documented
+in :ref:`staticfiles-from-cdn`.
Other Helpers
=============
@@ -251,7 +388,7 @@ files:
with :class:`~django.template.RequestContext` contexts.
- The builtin template tag :ttag:`static` which takes a path and
- joins it with the the static prefix :setting:`STATIC_URL`.
+ urljoins it with the static prefix :setting:`STATIC_URL`.
- The builtin template tag :ttag:`get_static_prefix` which populates a
template variable with the static prefix :setting:`STATIC_URL` to be
View
17 docs/ref/templates/builtins.txt
@@ -2353,9 +2353,9 @@ static
.. highlight:: html+django
-To link to static files Django ships with a :ttag:`static` template tag. You
-can use this regardless if you're using :class:`~django.template.RequestContext`
-or not.
+To link to static files that are saved in :setting:`STATIC_ROOT` Django ships
+with a :ttag:`static` template tag. You can use this regardless if you're
+using :class:`~django.template.RequestContext` or not.
.. code-block:: html+django
@@ -2370,6 +2370,17 @@ It is also able to consume standard context variables, e.g. assuming a
{% load static %}
<link rel="stylesheet" href="{% static user_stylesheet %}" type="text/css" media="screen" />
+.. note::
+
+ The :mod:`staticfiles<django.contrib.staticfiles>` contrib app also ships
+ with a :ttag:`static template tag<staticfiles-static>` which uses
+ ``staticfiles'`` :setting:`STATICFILES_STORAGE` to build the URL of the
+ given path. Use that instead if you have an advanced use case such as
+ :ref:`using a cloud service to serve static files<staticfiles-from-cdn>`::
+
+ {% load static from staticfiles %}
+ <img src="{% static "images/hi.jpg" %}" />
+
.. templatetag:: get_static_prefix
get_static_prefix
View
23 docs/releases/1.4.txt
@@ -212,6 +212,29 @@ Additionally, it's now possible to define translatable URL patterns using
:ref:`url-internationalization` for more information about the language prefix
and how to internationalize URL patterns.
+``static`` template tag
+~~~~~~~~~~~~~~~~~~~~~~~
+
+The :mod:`staticfiles<django.contrib.staticfiles>` contrib app has now a new
+:ttag:`static template tag<staticfiles-static>` to refer to files saved with
+the :setting:`STATICFILES_STORAGE` storage backend. It'll use the storage
+``url`` method and therefore supports advanced features such as
+:ref:`serving files from a cloud service<staticfiles-from-cdn>`.
+
+``CachedStaticFilesStorage`` storage backend
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Additional to the `static template tag`_ the
+:mod:`staticfiles<django.contrib.staticfiles>` contrib app now has a
+:class:`~django.contrib.staticfiles.storage.CachedStaticFilesStorage` which
+caches the files it saves (when running the :djadmin:`collectstatic`
+management command) by appending the MD5 hash of the file's content to the
+filename. For example, the file ``css/styles.css`` would also be saved as
+``css/styles.55e7cbb9ba48.css``
+
+See the :class:`~django.contrib.staticfiles.storage.CachedStaticFilesStorage`
+docs for more information.
+
Minor features
~~~~~~~~~~~~~~
View
1 tests/regressiontests/staticfiles_tests/project/documents/cached/absolute.css
@@ -0,0 +1 @@
+@import url("/static/cached/styles.css");
View
1 tests/regressiontests/staticfiles_tests/project/documents/cached/denorm.css
@@ -0,0 +1 @@
+@import url("..//cached///styles.css");
View
0 tests/regressiontests/staticfiles_tests/project/documents/cached/other.css
No changes.
View
2 tests/regressiontests/staticfiles_tests/project/documents/cached/relative.css
@@ -0,0 +1,2 @@
+@import url("../cached/styles.css");
+@import url("absolute.css");
View
1 tests/regressiontests/staticfiles_tests/project/documents/cached/styles.css
@@ -0,0 +1 @@
+@import url("cached/other.css");
View
1 tests/regressiontests/staticfiles_tests/project/documents/cached/url.css
@@ -0,0 +1 @@
+@import url("https://www.djangoproject.com/m/css/base.css");
View
1 tests/regressiontests/staticfiles_tests/project/site_media/static/testfile.txt
@@ -0,0 +1 @@
+Test!
View
193 tests/regressiontests/staticfiles_tests/tests.py
@@ -8,6 +8,7 @@
import tempfile
from StringIO import StringIO
+from django.template import loader, Context
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import default_storage
@@ -21,9 +22,25 @@
from django.contrib.staticfiles import finders, storage
TEST_ROOT = os.path.dirname(__file__)
+TEST_SETTINGS = {
+ 'DEBUG': True,
+ 'MEDIA_URL': '/media/',
+ 'STATIC_URL': '/static/',
+ 'MEDIA_ROOT': os.path.join(TEST_ROOT, 'project', 'site_media', 'media'),
+ 'STATIC_ROOT': os.path.join(TEST_ROOT, 'project', 'site_media', 'static'),
+ 'STATICFILES_DIRS': (
+ os.path.join(TEST_ROOT, 'project', 'documents'),
+ ('prefix', os.path.join(TEST_ROOT, 'project', 'prefixed')),
+ ),
+ 'STATICFILES_FINDERS': (
+ 'django.contrib.staticfiles.finders.FileSystemFinder',
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+ 'django.contrib.staticfiles.finders.DefaultStorageFinder',
+ ),
+}
-class StaticFilesTestCase(TestCase):
+class BaseStaticFilesTestCase(object):
"""
Test case with a couple utility assertions.
"""
@@ -32,6 +49,7 @@ def setUp(self):
# gets accessed (by some other test), it evaluates settings.MEDIA_ROOT,
# since we're planning on changing that we need to clear out the cache.
default_storage._wrapped = empty
+ storage.staticfiles_storage._wrapped = empty
# To make sure SVN doesn't hangs itself with the non-ASCII characters
# during checkout, we actually create one file dynamically.
@@ -48,35 +66,34 @@ def assertFileContains(self, filepath, text):
def assertFileNotFound(self, filepath):
self.assertRaises(IOError, self._get_file, filepath)
-StaticFilesTestCase = override_settings(
- DEBUG = True,
- MEDIA_URL = '/media/',
- STATIC_URL = '/static/',
- MEDIA_ROOT = os.path.join(TEST_ROOT, 'project', 'site_media', 'media'),
- STATIC_ROOT = os.path.join(TEST_ROOT, 'project', 'site_media', 'static'),
- STATICFILES_DIRS = (
- os.path.join(TEST_ROOT, 'project', 'documents'),
- ('prefix', os.path.join(TEST_ROOT, 'project', 'prefixed')),
- ),
- STATICFILES_FINDERS = (
- 'django.contrib.staticfiles.finders.FileSystemFinder',
- 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
- 'django.contrib.staticfiles.finders.DefaultStorageFinder',
- ),
-)(StaticFilesTestCase)
+ def render_template(self, template, **kwargs):
+ if isinstance(template, basestring):
+ template = loader.get_template_from_string(template)
+ return template.render(Context(kwargs)).strip()
+
+ def assertTemplateRenders(self, template, result, **kwargs):
+ self.assertEqual(self.render_template(template, **kwargs), result)
+ def assertTemplateRaises(self, exc, template, result, **kwargs):
+ self.assertRaises(exc, self.assertTemplateRenders, template, result, **kwargs)
-class BuildStaticTestCase(StaticFilesTestCase):
+
+class StaticFilesTestCase(BaseStaticFilesTestCase, TestCase):
+ pass
+StaticFilesTestCase = override_settings(**TEST_SETTINGS)(StaticFilesTestCase)
+
+
+class BaseCollectionTestCase(BaseStaticFilesTestCase):
"""
- Tests shared by all file-resolving features (collectstatic,
+ Tests shared by all file finding features (collectstatic,
findstatic, and static serve view).
This relies on the asserts defined in UtilityAssertsTestCase, but
is separated because some test cases need those asserts without
all these tests.
"""
def setUp(self):
- super(BuildStaticTestCase, self).setUp()
+ super(BaseCollectionTestCase, self).setUp()
self.old_root = settings.STATIC_ROOT
settings.STATIC_ROOT = tempfile.mkdtemp()
self.run_collectstatic()
@@ -86,7 +103,7 @@ def setUp(self):
def tearDown(self):
settings.STATIC_ROOT = self.old_root
- super(BuildStaticTestCase, self).tearDown()
+ super(BaseCollectionTestCase, self).tearDown()
def run_collectstatic(self, **kwargs):
call_command('collectstatic', interactive=False, verbosity='0',
@@ -99,6 +116,10 @@ def _get_file(self, filepath):
return f.read()
+class CollectionTestCase(BaseCollectionTestCase, StaticFilesTestCase):
+ pass
+
+
class TestDefaults(object):
"""
A few standard test cases.
@@ -142,7 +163,7 @@ def test_camelcase_filenames(self):
self.assertFileContains(u'test/camelCase.txt', u'camelCase')
-class TestFindStatic(BuildStaticTestCase, TestDefaults):
+class TestFindStatic(CollectionTestCase, TestDefaults):
"""
Test ``findstatic`` management command.
"""
@@ -171,12 +192,12 @@ def test_all_files(self):
lines = [l.strip() for l in sys.stdout.readlines()]
finally:
sys.stdout = _stdout
- self.assertEqual(len(lines), 3) # three because there is also the "Found <file> here" line
+ self.assertEqual(len(lines), 3) # three because there is also the "Found <file> here" line
self.assertTrue('project' in lines[1])
self.assertTrue('apps' in lines[2])
-class TestBuildStatic(BuildStaticTestCase, TestDefaults):
+class TestCollection(CollectionTestCase, TestDefaults):
"""
Test ``collectstatic`` management command.
"""
@@ -195,27 +216,27 @@ def test_common_ignore_patterns(self):
self.assertFileNotFound('test/CVS')
-class TestBuildStaticClear(BuildStaticTestCase):
+class TestCollectionClear(CollectionTestCase):
"""
Test the ``--clear`` option of the ``collectstatic`` managemenet command.
"""
def run_collectstatic(self, **kwargs):
clear_filepath = os.path.join(settings.STATIC_ROOT, 'cleared.txt')
with open(clear_filepath, 'w') as f:
f.write('should be cleared')
- super(TestBuildStaticClear, self).run_collectstatic(clear=True)
+ super(TestCollectionClear, self).run_collectstatic(clear=True)
def test_cleared_not_found(self):
self.assertFileNotFound('cleared.txt')
-class TestBuildStaticExcludeNoDefaultIgnore(BuildStaticTestCase, TestDefaults):
+class TestCollectionExcludeNoDefaultIgnore(CollectionTestCase, TestDefaults):
"""
Test ``--exclude-dirs`` and ``--no-default-ignore`` options of the
``collectstatic`` management command.
"""
def run_collectstatic(self):
- super(TestBuildStaticExcludeNoDefaultIgnore, self).run_collectstatic(
+ super(TestCollectionExcludeNoDefaultIgnore, self).run_collectstatic(
use_default_ignore_patterns=False)
def test_no_common_ignore_patterns(self):
@@ -238,27 +259,98 @@ def test_no_files_created(self):
self.assertEqual(os.listdir(settings.STATIC_ROOT), [])
-class TestBuildStaticDryRun(BuildStaticTestCase, TestNoFilesCreated):
+class TestCollectionDryRun(CollectionTestCase, TestNoFilesCreated):
"""
Test ``--dry-run`` option for ``collectstatic`` management command.
"""
def run_collectstatic(self):
- super(TestBuildStaticDryRun, self).run_collectstatic(dry_run=True)
+ super(TestCollectionDryRun, self).run_collectstatic(dry_run=True)
-class TestBuildStaticNonLocalStorage(BuildStaticTestCase, TestNoFilesCreated):
+class TestCollectionNonLocalStorage(CollectionTestCase, TestNoFilesCreated):
"""
Tests for #15035
"""
pass
-TestBuildStaticNonLocalStorage = override_settings(
+TestCollectionNonLocalStorage = override_settings(
STATICFILES_STORAGE='regressiontests.staticfiles_tests.storage.DummyStorage',
-)(TestBuildStaticNonLocalStorage)
+)(TestCollectionNonLocalStorage)
+
+
+class TestCollectionCachedStorage(BaseCollectionTestCase,
+ BaseStaticFilesTestCase, TestCase):
+ """
+ Tests for the Cache busting storage
+ """
+ def cached_file_path(self, relpath):
+ template = "{%% load static from staticfiles %%}{%% static '%s' %%}"
+ fullpath = self.render_template(template % relpath)
+ return fullpath.replace(settings.STATIC_URL, '')
+
+ def test_template_tag_return(self):
+ """
+ Test the CachedStaticFilesStorage backend.
+ """
+ self.assertTemplateRaises(ValueError, """
+ {% load static from staticfiles %}{% static "does/not/exist.png" %}
+ """, "/static/does/not/exist.png")
+ self.assertTemplateRenders("""
+ {% load static from staticfiles %}{% static "test/file.txt" %}
+ """, "/static/test/file.dad0999e4f8f.txt")
+ self.assertTemplateRenders("""
+ {% load static from staticfiles %}{% static "cached/styles.css" %}
+ """, "/static/cached/styles.5653c259030b.css")
+
+ def test_template_tag_simple_content(self):
+ relpath = self.cached_file_path("cached/styles.css")
+ self.assertEqual(relpath, "cached/styles.5653c259030b.css")
+ with storage.staticfiles_storage.open(relpath) as relfile:
+ content = relfile.read()
+ self.assertFalse("cached/other.css" in content, content)
+ self.assertTrue("/static/cached/other.d41d8cd98f00.css" in content)
+
+ def test_template_tag_absolute(self):
+ relpath = self.cached_file_path("cached/absolute.css")
+ self.assertEqual(relpath, "cached/absolute.cc80cb5e2eb1.css")
+ with storage.staticfiles_storage.open(relpath) as relfile:
+ content = relfile.read()
+ self.assertFalse("/static/cached/styles.css" in content)
+ self.assertTrue("/static/cached/styles.5653c259030b.css" in content)
+
+ def test_template_tag_denorm(self):
+ relpath = self.cached_file_path("cached/denorm.css")
+ self.assertEqual(relpath, "cached/denorm.363de96e9b4b.css")
+ with storage.staticfiles_storage.open(relpath) as relfile:
+ content = relfile.read()
+ self.assertFalse("..//cached///styles.css" in content)
+ self.assertTrue("/static/cached/styles.5653c259030b.css" in content)
+
+ def test_template_tag_relative(self):
+ relpath = self.cached_file_path("cached/relative.css")
+ self.assertEqual(relpath, "cached/relative.298ff891a8d4.css")
+ with storage.staticfiles_storage.open(relpath) as relfile:
+ content = relfile.read()
+ self.assertFalse("../cached/styles.css" in content)
+ self.assertFalse('@import "styles.css"' in content)
+ self.assertTrue("/static/cached/styles.5653c259030b.css" in content)
+
+ def test_template_tag_url(self):
+ relpath = self.cached_file_path("cached/url.css")
+ self.assertEqual(relpath, "cached/url.615e21601e4b.css")
+ with storage.staticfiles_storage.open(relpath) as relfile:
+ self.assertTrue("https://" in relfile.read())
+
+# we set DEBUG to False here since the template tag wouldn't work otherwise
+TestCollectionCachedStorage = override_settings(**dict(TEST_SETTINGS,
+ STATICFILES_STORAGE='django.contrib.staticfiles.storage.CachedStaticFilesStorage',
+ DEBUG=False,
+))(TestCollectionCachedStorage)
if sys.platform != 'win32':
- class TestBuildStaticLinks(BuildStaticTestCase, TestDefaults):
+
+ class TestCollectionLinks(CollectionTestCase, TestDefaults):
"""
Test ``--link`` option for ``collectstatic`` management command.
@@ -267,7 +359,7 @@ class TestBuildStaticLinks(BuildStaticTestCase, TestDefaults):
``--link`` does not change the file-selection semantics.
"""
def run_collectstatic(self):
- super(TestBuildStaticLinks, self).run_collectstatic(link=True)
+ super(TestCollectionLinks, self).run_collectstatic(link=True)
def test_links_created(self):
"""
@@ -312,6 +404,7 @@ class TestServeStaticWithDefaultURL(TestServeStatic, TestDefaults):
"""
pass
+
class TestServeStaticWithURLHelper(TestServeStatic, TestDefaults):
"""
Test static asset serving view with staticfiles_urlpatterns helper.
@@ -399,22 +492,28 @@ def test_get_finder(self):
finders.FileSystemFinder))
def test_get_finder_bad_classname(self):
- self.assertRaises(ImproperlyConfigured,
- finders.get_finder, 'django.contrib.staticfiles.finders.FooBarFinder')
+ self.assertRaises(ImproperlyConfigured, finders.get_finder,
+ 'django.contrib.staticfiles.finders.FooBarFinder')
def test_get_finder_bad_module(self):
self.assertRaises(ImproperlyConfigured,
finders.get_finder, 'foo.bar.FooBarFinder')
-
-class TestStaticfilesDirsType(TestCase):
- """
- We can't determine if STATICFILES_DIRS is set correctly just by looking at
- the type, but we can determine if it's definitely wrong.
- """
def test_non_tuple_raises_exception(self):
- self.assertRaises(ImproperlyConfigured, finders.FileSystemFinder)
+ """
+ We can't determine if STATICFILES_DIRS is set correctly just by
+ looking at the type, but we can determine if it's definitely wrong.
+ """
+ with self.settings(STATICFILES_DIRS='a string'):
+ self.assertRaises(ImproperlyConfigured, finders.FileSystemFinder)
+
+
+class TestTemplateTag(StaticFilesTestCase):
-TestStaticfilesDirsType = override_settings(
- STATICFILES_DIRS = 'a string',
-)(TestStaticfilesDirsType)
+ def test_template_tag(self):
+ self.assertTemplateRenders("""
+ {% load static from staticfiles %}{% static "does/not/exist.png" %}
+ """, "/static/does/not/exist.png")
+ self.assertTemplateRenders("""
+ {% load static from staticfiles %}{% static "testfile.txt" %}
+ """, "/static/testfile.txt")

0 comments on commit 1d32bdd

Please sign in to comment.