Fixed #15252 -- Added static template tag and CachedStaticFilesStorage 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
This commit is contained in:
Jannis Leidel 2011-08-11 14:07:39 +00:00
parent e9a909e30a
commit 1d32bdd3c9
33 changed files with 646 additions and 148 deletions

View File

@ -1,6 +1,7 @@
from django import forms from django import forms
from django.contrib.admin.util import (flatten_fieldsets, lookup_field, from django.contrib.admin.util import (flatten_fieldsets, lookup_field,
display_for_field, label_for_field, help_text_for_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.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields.related import ManyToManyRel from django.db.models.fields.related import ManyToManyRel
@ -75,7 +76,7 @@ class Fieldset(object):
def _media(self): def _media(self):
if 'collapse' in self.classes: if 'collapse' in self.classes:
js = ['jquery.min.js', 'jquery.init.js', 'collapse.min.js'] 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() return forms.Media()
media = property(_media) media = property(_media)

View File

@ -6,6 +6,7 @@ from django.forms.models import (modelform_factory, modelformset_factory,
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets, helpers 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.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.contrib import messages
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
@ -350,7 +351,8 @@ class ModelAdmin(BaseModelAdmin):
return self.get_urls() return self.get_urls()
urls = property(urls) urls = property(urls)
def _media(self): @property
def media(self):
js = [ js = [
'core.js', 'core.js',
'admin/RelatedObjectLookups.js', 'admin/RelatedObjectLookups.js',
@ -363,8 +365,7 @@ class ModelAdmin(BaseModelAdmin):
js.extend(['urlify.js', 'prepopulate.min.js']) js.extend(['urlify.js', 'prepopulate.min.js'])
if self.opts.get_ordered_objects(): if self.opts.get_ordered_objects():
js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.js']) js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.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])
media = property(_media)
def has_add_permission(self, request): def has_add_permission(self, request):
""" """
@ -1322,14 +1323,14 @@ class InlineModelAdmin(BaseModelAdmin):
if self.verbose_name_plural is None: if self.verbose_name_plural is None:
self.verbose_name_plural = self.model._meta.verbose_name_plural 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'] js = ['jquery.min.js', 'jquery.init.js', 'inlines.min.js']
if self.prepopulated_fields: if self.prepopulated_fields:
js.extend(['urlify.js', 'prepopulate.min.js']) js.extend(['urlify.js', 'prepopulate.min.js'])
if self.filter_vertical or self.filter_horizontal: if self.filter_vertical or self.filter_horizontal:
js.extend(['SelectBox.js', 'SelectFilter2.js']) js.extend(['SelectBox.js', 'SelectFilter2.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])
media = property(_media)
def get_formset(self, request, obj=None, **kwargs): def get_formset(self, request, obj=None, **kwargs):
"""Returns a BaseInlineFormSet class for use in admin add/change views.""" """Returns a BaseInlineFormSet class for use in admin add/change views."""

View File

@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %} {% extends "admin/base_site.html" %}
{% load i18n static admin_modify %} {% load i18n admin_static admin_modify %}
{% load url from future %} {% load url from future %}
{% block extrahead %}{{ block.super }} {% block extrahead %}{{ block.super }}
{% url 'admin:jsi18n' as jsi18nurl %} {% url 'admin:jsi18n' as jsi18nurl %}

View File

@ -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 %}> <html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
<head> <head>
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>

View File

@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %} {% extends "admin/base_site.html" %}
{% load i18n static admin_modify %} {% load i18n admin_static admin_modify %}
{% load url from future %} {% load url from future %}
{% block extrahead %}{{ block.super }} {% block extrahead %}{{ block.super }}

View File

@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %} {% extends "admin/base_site.html" %}
{% load i18n static admin_list %} {% load i18n admin_static admin_list %}
{% load url from future %} {% load url from future %}
{% block extrastyle %} {% block extrastyle %}
{{ block.super }} {{ block.super }}

View File

@ -1,4 +1,4 @@
{% load i18n static %} {% load i18n admin_static %}
{% if result_hidden_fields %} {% if result_hidden_fields %}
<div class="hiddenfields">{# DIV for HTML validation #} <div class="hiddenfields">{# DIV for HTML validation #}
{% for item in result_hidden_fields %}{{ item }}{% endfor %} {% for item in result_hidden_fields %}{{ item }}{% endfor %}

View File

@ -1,4 +1,4 @@
{% load i18n static %} {% load i18n admin_static %}
<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group"> <div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
<h2>{{ inline_admin_formset.opts.verbose_name_plural|title }}</h2> <h2>{{ inline_admin_formset.opts.verbose_name_plural|title }}</h2>
{{ inline_admin_formset.formset.management_form }} {{ inline_admin_formset.formset.management_form }}

View File

@ -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="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
<div class="tabular inline-related {% if forloop.last %}last-related{% endif %}"> <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
{{ inline_admin_formset.formset.management_form }} {{ inline_admin_formset.formset.management_form }}

View File

@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %} {% 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 %} {% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}" />{% endblock %}

View File

@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %} {% 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 %} {% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/login.css" %}" />{% endblock %}

View File

@ -1,4 +1,4 @@
{% load i18n static %} {% load i18n admin_static %}
{% if cl.search_fields %} {% if cl.search_fields %}
<div id="toolbar"><form id="changelist-search" action="" method="get"> <div id="toolbar"><form id="changelist-search" action="" method="get">
<div><!-- DIV needed for valid HTML --> <div><!-- DIV needed for valid HTML -->

View File

@ -3,9 +3,9 @@ import datetime
from django.contrib.admin.util import lookup_field, display_for_field, label_for_field 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, from django.contrib.admin.views.main import (ALL_VAR, EMPTY_CHANGELIST_VALUE,
ORDER_VAR, PAGE_VAR, SEARCH_VAR) ORDER_VAR, PAGE_VAR, SEARCH_VAR)
from django.contrib.admin.templatetags.admin_static import static
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from django.templatetags.static import static
from django.utils import formats from django.utils import formats
from django.utils.html import escape, conditional_escape from django.utils.html import escape, conditional_escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe

View File

@ -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 File

@ -4,16 +4,17 @@ Form Widget classes specific to the Django admin site.
import copy import copy
from django import forms from django import forms
from django.contrib.admin.templatetags.admin_static import static
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.forms.widgets import RadioFieldRenderer from django.forms.widgets import RadioFieldRenderer
from django.forms.util import flatatt from django.forms.util import flatatt
from django.templatetags.static import static
from django.utils.html import escape from django.utils.html import escape
from django.utils.text import Truncator from django.utils.text import Truncator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.encoding import force_unicode from django.utils.encoding import force_unicode
class FilteredSelectMultiple(forms.SelectMultiple): class FilteredSelectMultiple(forms.SelectMultiple):
""" """
A SelectMultiple with a JavaScript filter interface. A SelectMultiple with a JavaScript filter interface.
@ -21,9 +22,10 @@ class FilteredSelectMultiple(forms.SelectMultiple):
Note that the resulting JavaScript assumes that the jsi18n Note that the resulting JavaScript assumes that the jsi18n
catalog has been loaded in the page catalog has been loaded in the page
""" """
class Media: @property
js = ["admin/js/%s" % path def media(self):
for path in ["core.js", "SelectBox.js", "SelectFilter2.js"]] 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=()): def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
self.verbose_name = verbose_name self.verbose_name = verbose_name
@ -31,9 +33,11 @@ class FilteredSelectMultiple(forms.SelectMultiple):
super(FilteredSelectMultiple, self).__init__(attrs, choices) super(FilteredSelectMultiple, self).__init__(attrs, choices)
def render(self, name, value, attrs=None, choices=()): def render(self, name, value, attrs=None, choices=()):
if attrs is None: attrs = {} if attrs is None:
attrs = {}
attrs['class'] = 'selectfilter' 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 = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)]
output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {') output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
# TODO: "id_" is hard-coded here. This should instead use the correct # TODO: "id_" is hard-coded here. This should instead use the correct
@ -43,15 +47,21 @@ class FilteredSelectMultiple(forms.SelectMultiple):
return mark_safe(u''.join(output)) return mark_safe(u''.join(output))
class AdminDateWidget(forms.DateInput): 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): def __init__(self, attrs={}, format=None):
super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'}, format=format) super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'}, format=format)
class AdminTimeWidget(forms.TimeInput): 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): def __init__(self, attrs={}, format=None):
super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'}, format=format) super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'}, format=format)
@ -232,9 +242,9 @@ class RelatedFieldWidgetWrapper(forms.Widget):
memo[id(self)] = obj memo[id(self)] = obj
return obj return obj
def _media(self): @property
def media(self):
return self.widget.media return self.widget.media
media = property(_media)
def render(self, name, value, *args, **kwargs): def render(self, name, value, *args, **kwargs):
rel_to = self.rel.to rel_to = self.rel.to

View File

@ -28,7 +28,7 @@ class BaseFinder(object):
""" """
raise NotImplementedError() raise NotImplementedError()
def list(self, ignore_patterns=[]): def list(self, ignore_patterns):
""" """
Given an optional list of paths to ignore, this should return Given an optional list of paths to ignore, this should return
a two item iterable consisting of the relative path and storage a two item iterable consisting of the relative path and storage

View File

@ -4,12 +4,11 @@ import os
import sys import sys
from optparse import make_option from optparse import make_option
from django.conf import settings from django.core.files.storage import FileSystemStorage
from django.core.files.storage import FileSystemStorage, get_storage_class
from django.core.management.base import CommandError, NoArgsCommand from django.core.management.base import CommandError, NoArgsCommand
from django.utils.encoding import smart_str, smart_unicode 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): class Command(NoArgsCommand):
@ -18,32 +17,39 @@ class Command(NoArgsCommand):
locations to the settings.STATIC_ROOT. locations to the settings.STATIC_ROOT.
""" """
option_list = NoArgsCommand.option_list + ( option_list = NoArgsCommand.option_list + (
make_option('--noinput', action='store_false', dest='interactive', make_option('--noinput',
default=True, help="Do NOT prompt the user for input of any kind."), 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=[], make_option('-i', '--ignore', action='append', default=[],
dest='ignore_patterns', metavar='PATTERN', dest='ignore_patterns', metavar='PATTERN',
help="Ignore files or directories matching this glob-style " help="Ignore files or directories matching this glob-style "
"pattern. Use multiple times to ignore more."), "pattern. Use multiple times to ignore more."),
make_option('-n', '--dry-run', action='store_true', dest='dry_run', make_option('-n', '--dry-run',
default=False, help="Do everything except modify the filesystem."), action='store_true', dest='dry_run', default=False,
make_option('-c', '--clear', action='store_true', dest='clear', help="Do everything except modify the filesystem."),
default=False, help="Clear the existing files using the storage " 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."), "before trying to copy or link the original file."),
make_option('-l', '--link', action='store_true', dest='link', make_option('-l', '--link',
default=False, help="Create a symbolic link to each file instead of copying."), 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', make_option('--no-default-ignore', action='store_false',
dest='use_default_ignore_patterns', default=True, dest='use_default_ignore_patterns', default=True,
help="Don't ignore the common private glob-style patterns 'CVS', " help="Don't ignore the common private glob-style patterns 'CVS', "
"'.*' and '*~'."), "'.*' 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): def __init__(self, *args, **kwargs):
super(NoArgsCommand, self).__init__(*args, **kwargs) super(NoArgsCommand, self).__init__(*args, **kwargs)
self.copied_files = [] self.copied_files = []
self.symlinked_files = [] self.symlinked_files = []
self.unmodified_files = [] self.unmodified_files = []
self.storage = get_storage_class(settings.STATICFILES_STORAGE)() self.storage = storage.staticfiles_storage
try: try:
self.storage.path('') self.storage.path('')
except NotImplementedError: except NotImplementedError:
@ -64,6 +70,7 @@ class Command(NoArgsCommand):
self.interactive = options['interactive'] self.interactive = options['interactive']
self.symlink = options['link'] self.symlink = options['link']
self.verbosity = int(options.get('verbosity', 1)) self.verbosity = int(options.get('verbosity', 1))
self.post_process = options['post_process']
if self.symlink: if self.symlink:
if sys.platform == 'win32': if sys.platform == 'win32':
@ -104,9 +111,10 @@ Type 'yes' to continue, or 'no' to cancel: """
handler = { handler = {
True: self.link_file, True: self.link_file,
False: self.copy_file False: self.copy_file,
}[self.symlink] }[self.symlink]
found_files = []
for finder in finders.get_finders(): for finder in finders.get_finders():
for path, storage in finder.list(self.ignore_patterns): for path, storage in finder.list(self.ignore_patterns):
# Prefix the relative path if the source storage contains it # Prefix the relative path if the source storage contains it
@ -114,19 +122,35 @@ Type 'yes' to continue, or 'no' to cancel: """
prefixed_path = os.path.join(storage.prefix, path) prefixed_path = os.path.join(storage.prefix, path)
else: else:
prefixed_path = path prefixed_path = path
found_files.append(prefixed_path)
handler(path, prefixed_path, storage) 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) unmodified_count = len(self.unmodified_files)
if self.verbosity >= 1: if self.verbosity >= 1:
self.stdout.write(smart_str(u"\n%s static file%s %s %s%s.\n" template = ("\n%(actual_count)s %(identifier)s %(action)s"
% (actual_count, "%(destination)s%(unmodified)s.\n")
actual_count != 1 and 's' or '', summary = template % {
self.symlink and 'symlinked' or 'copied', 'actual_count': actual_count,
destination_path and "to '%s'" 'identifier': 'static file' + (actual_count > 1 and 's' or ''),
% destination_path or '', 'action': self.symlink and 'symlinked' or 'copied',
unmodified_count and ' (%s unmodified)' 'destination': (destination_path and " to '%s'"
% unmodified_count or ''))) % 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): def log(self, msg, level=2):
""" """
@ -146,7 +170,8 @@ Type 'yes' to continue, or 'no' to cancel: """
for f in files: for f in files:
fpath = os.path.join(path, f) fpath = os.path.join(path, f)
if self.dry_run: 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: else:
self.log(u"Deleting '%s'" % smart_unicode(fpath), level=1) self.log(u"Deleting '%s'" % smart_unicode(fpath), level=1)
self.storage.delete(fpath) self.storage.delete(fpath)
@ -159,7 +184,8 @@ Type 'yes' to continue, or 'no' to cancel: """
if self.storage.exists(prefixed_path): if self.storage.exists(prefixed_path):
try: try:
# When was the target file modified last time? # 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): except (OSError, NotImplementedError):
# The storage doesn't support ``modified_time`` or failed # The storage doesn't support ``modified_time`` or failed
pass pass
@ -177,8 +203,10 @@ Type 'yes' to continue, or 'no' to cancel: """
full_path = None full_path = None
# Skip the file if the source file is younger # Skip the file if the source file is younger
if target_last_modified >= source_last_modified: if target_last_modified >= source_last_modified:
if not ((self.symlink and full_path and not os.path.islink(full_path)) or if not ((self.symlink and full_path
(not self.symlink and full_path and os.path.islink(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: if prefixed_path not in self.unmodified_files:
self.unmodified_files.append(prefixed_path) self.unmodified_files.append(prefixed_path)
self.log(u"Skipping '%s' (not modified)" % path) self.log(u"Skipping '%s' (not modified)" % path)

View File

@ -1,10 +1,20 @@
import hashlib
import os import os
from django.conf import settings import posixpath
from django.core.exceptions import ImproperlyConfigured import re
from django.core.files.storage import FileSystemStorage
from django.utils.importlib import import_module
from django.contrib.staticfiles import utils 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.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.utils import check_settings, matches_patterns
class StaticFilesStorage(FileSystemStorage): class StaticFilesStorage(FileSystemStorage):
@ -26,8 +36,148 @@ class StaticFilesStorage(FileSystemStorage):
if base_url is None: if base_url is None:
raise ImproperlyConfigured("You're using the staticfiles app " raise ImproperlyConfigured("You're using the staticfiles app "
"without having set the STATIC_URL setting.") "without having set the STATIC_URL setting.")
utils.check_settings() check_settings()
super(StaticFilesStorage, self).__init__(location, base_url, *args, **kwargs) 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): class AppStaticStorage(FileSystemStorage):
@ -47,3 +197,10 @@ class AppStaticStorage(FileSystemStorage):
mod_path = os.path.dirname(mod.__file__) mod_path = os.path.dirname(mod.__file__)
location = os.path.join(mod_path, self.source_dir) location = os.path.join(mod_path, self.source_dir)
super(AppStaticStorage, self).__init__(location, *args, **kwargs) 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 File

@ -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 File

@ -3,30 +3,34 @@ import fnmatch
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured 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 Return True or False depending on whether the ``path`` should be
ignored (if it matches any pattern in ``ignore_patterns``). 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): if fnmatch.fnmatchcase(path, pattern):
return True return True
return False 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 Recursively walk the storage directories yielding the paths
of all files that should be copied. of all files that should be copied.
""" """
if ignore_patterns is None:
ignore_patterns = []
directories, files = storage.listdir(location) directories, files = storage.listdir(location)
for fn in files: for fn in files:
if is_ignored(fn, ignore_patterns): if matches_patterns(fn, ignore_patterns):
continue continue
if location: if location:
fn = os.path.join(location, fn) fn = os.path.join(location, fn)
yield fn yield fn
for dir in directories: for dir in directories:
if is_ignored(dir, ignore_patterns): if matches_patterns(dir, ignore_patterns):
continue continue
if location: if location:
dir = os.path.join(location, dir) dir = os.path.join(location, dir)

View File

@ -70,7 +70,7 @@ Basic usage
<img src="{{ STATIC_URL }}images/hi.jpg" /> <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. alternate method using a template tag.
Deploying static files in a nutshell 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 directly in your templates. This means that a switch of static files servers
only requires changing that single value. Much better! 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. templates: a context processor and a template tag.
With a context processor With a context processor
@ -180,14 +180,19 @@ 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 To see how that works, and to read more details, check out
:ref:`subclassing-context-requestcontext`. :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 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 .. code-block:: html+django
{% load static %} {% load staticfiles %}
<img src="{% static "images/hi.jpg" %}" /> <img src="{% static "images/hi.jpg" %}" />
It is also able to consume standard context variables, e.g. assuming a It is also able to consume standard context variables, e.g. assuming a
@ -195,30 +200,21 @@ It is also able to consume standard context variables, e.g. assuming a
.. code-block:: html+django .. code-block:: html+django
{% load static %} {% load staticfiles %}
<link rel="stylesheet" href="{% static user_stylesheet %}" type="text/css" media="screen" /> <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 .. note::
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 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.
{% load static %} .. _`urlparse.urljoin()`: http://docs.python.org/library/urlparse.html#urlparse.urljoin
<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
{% load static %}
{% get_static_prefix as STATIC_PREFIX %}
<img src="{{ STATIC_PREFIX }}images/hi.jpg" />
<img src="{{ STATIC_PREFIX }}images/hi2.jpg" />
.. _staticfiles-development: .. _staticfiles-development:

View File

@ -68,7 +68,9 @@ in a ``'downloads'`` subdirectory of :setting:`STATIC_ROOT`.
This would allow you to refer to the local file This would allow you to refer to the local file
``'/opt/webfiles/stats/polls_20101022.tar.gz'`` with ``'/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"> <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 The file storage engine to use when collecting static files with the
:djadmin:`collectstatic` management command. :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`. For an example, see :ref:`staticfiles-from-cdn`.
.. setting:: STATICFILES_FINDERS .. 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 :setting:`STATICFILES_DIRS` and in the ``'static'`` directory of apps
specified by the :setting:`INSTALLED_APPS` setting. 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: Some commonly used options are:
.. django-admin-option:: --noinput .. django-admin-option:: --noinput
@ -169,6 +186,13 @@ Some commonly used options are:
Create a symbolic link to each file instead of copying. 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 .. django-admin-option:: --no-default-ignore
Don't ignore the common private glob-style patterns ``'CVS'``, ``'.*'`` Don't ignore the common private glob-style patterns ``'CVS'``, ``'.*'``
@ -237,7 +261,120 @@ Example usage::
django-admin.py runserver --insecure 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 Other Helpers
============= =============
@ -251,7 +388,7 @@ files:
with :class:`~django.template.RequestContext` contexts. with :class:`~django.template.RequestContext` contexts.
- The builtin template tag :ttag:`static` which takes a path and - 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 - The builtin template tag :ttag:`get_static_prefix` which populates a
template variable with the static prefix :setting:`STATIC_URL` to be template variable with the static prefix :setting:`STATIC_URL` to be

View File

@ -2353,9 +2353,9 @@ static
.. highlight:: html+django .. highlight:: html+django
To link to static files Django ships with a :ttag:`static` template tag. You To link to static files that are saved in :setting:`STATIC_ROOT` Django ships
can use this regardless if you're using :class:`~django.template.RequestContext` with a :ttag:`static` template tag. You can use this regardless if you're
or not. using :class:`~django.template.RequestContext` or not.
.. code-block:: html+django .. code-block:: html+django
@ -2370,6 +2370,17 @@ It is also able to consume standard context variables, e.g. assuming a
{% load static %} {% load static %}
<link rel="stylesheet" href="{% static user_stylesheet %}" type="text/css" media="screen" /> <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 .. templatetag:: get_static_prefix
get_static_prefix get_static_prefix

View File

@ -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 :ref:`url-internationalization` for more information about the language prefix
and how to internationalize URL patterns. 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 Minor features
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~

View File

@ -0,0 +1 @@
@import url("/static/cached/styles.css");

View File

@ -0,0 +1 @@
@import url("..//cached///styles.css");

View File

@ -0,0 +1,2 @@
@import url("../cached/styles.css");
@import url("absolute.css");

View File

@ -0,0 +1 @@
@import url("cached/other.css");

View File

@ -0,0 +1 @@
@import url("https://www.djangoproject.com/m/css/base.css");

View File

@ -8,6 +8,7 @@ import sys
import tempfile import tempfile
from StringIO import StringIO from StringIO import StringIO
from django.template import loader, Context
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
@ -21,9 +22,25 @@ from django.utils._os import rmtree_errorhandler
from django.contrib.staticfiles import finders, storage from django.contrib.staticfiles import finders, storage
TEST_ROOT = os.path.dirname(__file__) 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. Test case with a couple utility assertions.
""" """
@ -32,6 +49,7 @@ class StaticFilesTestCase(TestCase):
# gets accessed (by some other test), it evaluates settings.MEDIA_ROOT, # 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. # since we're planning on changing that we need to clear out the cache.
default_storage._wrapped = empty default_storage._wrapped = empty
storage.staticfiles_storage._wrapped = empty
# To make sure SVN doesn't hangs itself with the non-ASCII characters # To make sure SVN doesn't hangs itself with the non-ASCII characters
# during checkout, we actually create one file dynamically. # during checkout, we actually create one file dynamically.
@ -48,27 +66,26 @@ class StaticFilesTestCase(TestCase):
def assertFileNotFound(self, filepath): def assertFileNotFound(self, filepath):
self.assertRaises(IOError, self._get_file, filepath) self.assertRaises(IOError, self._get_file, filepath)
StaticFilesTestCase = override_settings( def render_template(self, template, **kwargs):
DEBUG = True, if isinstance(template, basestring):
MEDIA_URL = '/media/', template = loader.get_template_from_string(template)
STATIC_URL = '/static/', return template.render(Context(kwargs)).strip()
MEDIA_ROOT = os.path.join(TEST_ROOT, 'project', 'site_media', 'media'),
STATIC_ROOT = os.path.join(TEST_ROOT, 'project', 'site_media', 'static'), def assertTemplateRenders(self, template, result, **kwargs):
STATICFILES_DIRS = ( self.assertEqual(self.render_template(template, **kwargs), result)
os.path.join(TEST_ROOT, 'project', 'documents'),
('prefix', os.path.join(TEST_ROOT, 'project', 'prefixed')), def assertTemplateRaises(self, exc, template, result, **kwargs):
), self.assertRaises(exc, self.assertTemplateRenders, template, result, **kwargs)
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'django.contrib.staticfiles.finders.DefaultStorageFinder',
),
)(StaticFilesTestCase)
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). findstatic, and static serve view).
This relies on the asserts defined in UtilityAssertsTestCase, but This relies on the asserts defined in UtilityAssertsTestCase, but
@ -76,7 +93,7 @@ class BuildStaticTestCase(StaticFilesTestCase):
all these tests. all these tests.
""" """
def setUp(self): def setUp(self):
super(BuildStaticTestCase, self).setUp() super(BaseCollectionTestCase, self).setUp()
self.old_root = settings.STATIC_ROOT self.old_root = settings.STATIC_ROOT
settings.STATIC_ROOT = tempfile.mkdtemp() settings.STATIC_ROOT = tempfile.mkdtemp()
self.run_collectstatic() self.run_collectstatic()
@ -86,7 +103,7 @@ class BuildStaticTestCase(StaticFilesTestCase):
def tearDown(self): def tearDown(self):
settings.STATIC_ROOT = self.old_root settings.STATIC_ROOT = self.old_root
super(BuildStaticTestCase, self).tearDown() super(BaseCollectionTestCase, self).tearDown()
def run_collectstatic(self, **kwargs): def run_collectstatic(self, **kwargs):
call_command('collectstatic', interactive=False, verbosity='0', call_command('collectstatic', interactive=False, verbosity='0',
@ -99,6 +116,10 @@ class BuildStaticTestCase(StaticFilesTestCase):
return f.read() return f.read()
class CollectionTestCase(BaseCollectionTestCase, StaticFilesTestCase):
pass
class TestDefaults(object): class TestDefaults(object):
""" """
A few standard test cases. A few standard test cases.
@ -142,7 +163,7 @@ class TestDefaults(object):
self.assertFileContains(u'test/camelCase.txt', u'camelCase') self.assertFileContains(u'test/camelCase.txt', u'camelCase')
class TestFindStatic(BuildStaticTestCase, TestDefaults): class TestFindStatic(CollectionTestCase, TestDefaults):
""" """
Test ``findstatic`` management command. Test ``findstatic`` management command.
""" """
@ -176,7 +197,7 @@ class TestFindStatic(BuildStaticTestCase, TestDefaults):
self.assertTrue('apps' in lines[2]) self.assertTrue('apps' in lines[2])
class TestBuildStatic(BuildStaticTestCase, TestDefaults): class TestCollection(CollectionTestCase, TestDefaults):
""" """
Test ``collectstatic`` management command. Test ``collectstatic`` management command.
""" """
@ -195,7 +216,7 @@ class TestBuildStatic(BuildStaticTestCase, TestDefaults):
self.assertFileNotFound('test/CVS') self.assertFileNotFound('test/CVS')
class TestBuildStaticClear(BuildStaticTestCase): class TestCollectionClear(CollectionTestCase):
""" """
Test the ``--clear`` option of the ``collectstatic`` managemenet command. Test the ``--clear`` option of the ``collectstatic`` managemenet command.
""" """
@ -203,19 +224,19 @@ class TestBuildStaticClear(BuildStaticTestCase):
clear_filepath = os.path.join(settings.STATIC_ROOT, 'cleared.txt') clear_filepath = os.path.join(settings.STATIC_ROOT, 'cleared.txt')
with open(clear_filepath, 'w') as f: with open(clear_filepath, 'w') as f:
f.write('should be cleared') 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): def test_cleared_not_found(self):
self.assertFileNotFound('cleared.txt') self.assertFileNotFound('cleared.txt')
class TestBuildStaticExcludeNoDefaultIgnore(BuildStaticTestCase, TestDefaults): class TestCollectionExcludeNoDefaultIgnore(CollectionTestCase, TestDefaults):
""" """
Test ``--exclude-dirs`` and ``--no-default-ignore`` options of the Test ``--exclude-dirs`` and ``--no-default-ignore`` options of the
``collectstatic`` management command. ``collectstatic`` management command.
""" """
def run_collectstatic(self): def run_collectstatic(self):
super(TestBuildStaticExcludeNoDefaultIgnore, self).run_collectstatic( super(TestCollectionExcludeNoDefaultIgnore, self).run_collectstatic(
use_default_ignore_patterns=False) use_default_ignore_patterns=False)
def test_no_common_ignore_patterns(self): def test_no_common_ignore_patterns(self):
@ -238,27 +259,98 @@ class TestNoFilesCreated(object):
self.assertEqual(os.listdir(settings.STATIC_ROOT), []) self.assertEqual(os.listdir(settings.STATIC_ROOT), [])
class TestBuildStaticDryRun(BuildStaticTestCase, TestNoFilesCreated): class TestCollectionDryRun(CollectionTestCase, TestNoFilesCreated):
""" """
Test ``--dry-run`` option for ``collectstatic`` management command. Test ``--dry-run`` option for ``collectstatic`` management command.
""" """
def run_collectstatic(self): 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 Tests for #15035
""" """
pass pass
TestBuildStaticNonLocalStorage = override_settings( TestCollectionNonLocalStorage = override_settings(
STATICFILES_STORAGE='regressiontests.staticfiles_tests.storage.DummyStorage', 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': if sys.platform != 'win32':
class TestBuildStaticLinks(BuildStaticTestCase, TestDefaults):
class TestCollectionLinks(CollectionTestCase, TestDefaults):
""" """
Test ``--link`` option for ``collectstatic`` management command. Test ``--link`` option for ``collectstatic`` management command.
@ -267,7 +359,7 @@ if sys.platform != 'win32':
``--link`` does not change the file-selection semantics. ``--link`` does not change the file-selection semantics.
""" """
def run_collectstatic(self): def run_collectstatic(self):
super(TestBuildStaticLinks, self).run_collectstatic(link=True) super(TestCollectionLinks, self).run_collectstatic(link=True)
def test_links_created(self): def test_links_created(self):
""" """
@ -312,6 +404,7 @@ class TestServeStaticWithDefaultURL(TestServeStatic, TestDefaults):
""" """
pass pass
class TestServeStaticWithURLHelper(TestServeStatic, TestDefaults): class TestServeStaticWithURLHelper(TestServeStatic, TestDefaults):
""" """
Test static asset serving view with staticfiles_urlpatterns helper. Test static asset serving view with staticfiles_urlpatterns helper.
@ -399,22 +492,28 @@ class TestMiscFinder(TestCase):
finders.FileSystemFinder)) finders.FileSystemFinder))
def test_get_finder_bad_classname(self): def test_get_finder_bad_classname(self):
self.assertRaises(ImproperlyConfigured, self.assertRaises(ImproperlyConfigured, finders.get_finder,
finders.get_finder, 'django.contrib.staticfiles.finders.FooBarFinder') 'django.contrib.staticfiles.finders.FooBarFinder')
def test_get_finder_bad_module(self): def test_get_finder_bad_module(self):
self.assertRaises(ImproperlyConfigured, self.assertRaises(ImproperlyConfigured,
finders.get_finder, 'foo.bar.FooBarFinder') 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): def test_non_tuple_raises_exception(self):
"""
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) self.assertRaises(ImproperlyConfigured, finders.FileSystemFinder)
TestStaticfilesDirsType = override_settings(
STATICFILES_DIRS = 'a string', class TestTemplateTag(StaticFilesTestCase):
)(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")