From 1d32bdd3c9586ff10d0799264105850fa7e3f512 Mon Sep 17 00:00:00 2001
From: Jannis Leidel <jannis@leidel.info>
Date: Thu, 11 Aug 2011 14:07:39 +0000
Subject: [PATCH] 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
---
 django/contrib/admin/helpers.py               |   3 +-
 django/contrib/admin/options.py               |  13 +-
 .../admin/auth/user/change_password.html      |   2 +-
 .../contrib/admin/templates/admin/base.html   |   2 +-
 .../admin/templates/admin/change_form.html    |   2 +-
 .../admin/templates/admin/change_list.html    |   2 +-
 .../templates/admin/change_list_results.html  |   2 +-
 .../templates/admin/edit_inline/stacked.html  |   2 +-
 .../templates/admin/edit_inline/tabular.html  |   2 +-
 .../contrib/admin/templates/admin/index.html  |   2 +-
 .../contrib/admin/templates/admin/login.html  |   2 +-
 .../admin/templates/admin/search_form.html    |   2 +-
 .../contrib/admin/templatetags/admin_list.py  |   2 +-
 .../admin/templatetags/admin_static.py        |  11 +
 django/contrib/admin/widgets.py               |  34 +--
 django/contrib/staticfiles/finders.py         |   2 +-
 .../management/commands/collectstatic.py      |  84 +++++---
 django/contrib/staticfiles/storage.py         | 171 +++++++++++++++-
 .../staticfiles/templatetags/__init__.py      |   0
 .../staticfiles/templatetags/staticfiles.py   |  13 ++
 django/contrib/staticfiles/utils.py           |  14 +-
 docs/howto/static-files.txt                   |  44 ++--
 docs/ref/contrib/staticfiles.txt              | 143 ++++++++++++-
 docs/ref/templates/builtins.txt               |  17 +-
 docs/releases/1.4.txt                         |  23 +++
 .../project/documents/cached/absolute.css     |   1 +
 .../project/documents/cached/denorm.css       |   1 +
 .../project/documents/cached/other.css        |   0
 .../project/documents/cached/relative.css     |   2 +
 .../project/documents/cached/styles.css       |   1 +
 .../project/documents/cached/url.css          |   1 +
 .../project/site_media/static/testfile.txt    |   1 +
 .../staticfiles_tests/tests.py                | 193 +++++++++++++-----
 33 files changed, 646 insertions(+), 148 deletions(-)
 create mode 100644 django/contrib/admin/templatetags/admin_static.py
 create mode 100644 django/contrib/staticfiles/templatetags/__init__.py
 create mode 100644 django/contrib/staticfiles/templatetags/staticfiles.py
 create mode 100644 tests/regressiontests/staticfiles_tests/project/documents/cached/absolute.css
 create mode 100644 tests/regressiontests/staticfiles_tests/project/documents/cached/denorm.css
 create mode 100644 tests/regressiontests/staticfiles_tests/project/documents/cached/other.css
 create mode 100644 tests/regressiontests/staticfiles_tests/project/documents/cached/relative.css
 create mode 100644 tests/regressiontests/staticfiles_tests/project/documents/cached/styles.css
 create mode 100644 tests/regressiontests/staticfiles_tests/project/documents/cached/url.css
 create mode 100644 tests/regressiontests/staticfiles_tests/project/site_media/static/testfile.txt

diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index d15716b909..04a3492e1c 100644
--- a/django/contrib/admin/helpers.py
+++ b/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 @@ class Fieldset(object):
     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)
 
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 35c3cde0fc..81e8ae5614 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -6,6 +6,7 @@ from django.forms.models import (modelform_factory, modelformset_factory,
 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 @@ class ModelAdmin(BaseModelAdmin):
         return self.get_urls()
     urls = property(urls)
 
-    def _media(self):
+    @property
+    def media(self):
         js = [
             'core.js',
             'admin/RelatedObjectLookups.js',
@@ -363,8 +365,7 @@ class ModelAdmin(BaseModelAdmin):
             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 @@ class InlineModelAdmin(BaseModelAdmin):
         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."""
diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html
index c280f50d60..b18b0aabf3 100644
--- a/django/contrib/admin/templates/admin/auth/user/change_password.html
+++ b/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 %}
diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html
index 928244053f..4b3c42934f 100644
--- a/django/contrib/admin/templates/admin/base.html
+++ b/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>
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index c5e428d174..56661e9a8e 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/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 }}
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
index 29af47ff79..24c6d8cced 100644
--- a/django/contrib/admin/templates/admin/change_list.html
+++ b/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 }}
diff --git a/django/contrib/admin/templates/admin/change_list_results.html b/django/contrib/admin/templates/admin/change_list_results.html
index 233d4e12d0..b1db6470c9 100644
--- a/django/contrib/admin/templates/admin/change_list_results.html
+++ b/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 %}
diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html
index 7e69450866..476e2613c5 100644
--- a/django/contrib/admin/templates/admin/edit_inline/stacked.html
+++ b/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 }}
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
index 829422711d..29db95a171 100644
--- a/django/contrib/admin/templates/admin/edit_inline/tabular.html
+++ b/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 }}
diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html
index 0f81a1a4fe..7164220ce8 100644
--- a/django/contrib/admin/templates/admin/index.html
+++ b/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 %}
 
diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html
index a95cfd8e6b..dbaa119224 100644
--- a/django/contrib/admin/templates/admin/login.html
+++ b/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 %}
 
diff --git a/django/contrib/admin/templates/admin/search_form.html b/django/contrib/admin/templates/admin/search_form.html
index 2b23a215fb..162b54addf 100644
--- a/django/contrib/admin/templates/admin/search_form.html
+++ b/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 -->
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 7a3a9b826a..0f5eafc24d 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -3,9 +3,9 @@ import datetime
 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
diff --git a/django/contrib/admin/templatetags/admin_static.py b/django/contrib/admin/templatetags/admin_static.py
new file mode 100644
index 0000000000..5ea3ba5838
--- /dev/null
+++ b/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)
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 038351e6da..0d1f2a9ff7 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -4,16 +4,17 @@ Form Widget classes specific to the Django admin site.
 
 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.
@@ -21,9 +22,10 @@ class FilteredSelectMultiple(forms.SelectMultiple):
     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
@@ -31,9 +33,11 @@ class FilteredSelectMultiple(forms.SelectMultiple):
         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 @@ class FilteredSelectMultiple(forms.SelectMultiple):
         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 @@ class RelatedFieldWidgetWrapper(forms.Widget):
         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
diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py
index ffb96e0af2..45bf4a1023 100644
--- a/django/contrib/staticfiles/finders.py
+++ b/django/contrib/staticfiles/finders.py
@@ -28,7 +28,7 @@ class BaseFinder(object):
         """
         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
diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py
index dd597ada07..18f889374b 100644
--- a/django/contrib/staticfiles/management/commands/collectstatic.py
+++ b/django/contrib/staticfiles/management/commands/collectstatic.py
@@ -4,12 +4,11 @@ import os
 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 @@ class Command(NoArgsCommand):
         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,9 +111,10 @@ Type 'yes' to continue, or 'no' to cancel: """
 
         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
@@ -114,19 +122,35 @@ Type 'yes' to continue, or 'no' to cancel: """
                     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 @@ Type 'yes' to continue, or 'no' to cancel: """
         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 @@ Type 'yes' to continue, or 'no' to cancel: """
         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 @@ Type 'yes' to continue, or 'no' to cancel: """
                         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)
diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py
index a69ae239df..d2f5cdd9d5 100644
--- a/django/contrib/staticfiles/storage.py
+++ b/django/contrib/staticfiles/storage.py
@@ -1,10 +1,20 @@
+import hashlib
 import os
-from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
-from django.core.files.storage import FileSystemStorage
-from django.utils.importlib import import_module
+import posixpath
+import re
 
-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):
@@ -26,8 +36,148 @@ class StaticFilesStorage(FileSystemStorage):
         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 @@ class AppStaticStorage(FileSystemStorage):
         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()
diff --git a/django/contrib/staticfiles/templatetags/__init__.py b/django/contrib/staticfiles/templatetags/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/django/contrib/staticfiles/templatetags/staticfiles.py b/django/contrib/staticfiles/templatetags/staticfiles.py
new file mode 100644
index 0000000000..788f06ec16
--- /dev/null
+++ b/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)
diff --git a/django/contrib/staticfiles/utils.py b/django/contrib/staticfiles/utils.py
index 9ff4bc4321..9aee928063 100644
--- a/django/contrib/staticfiles/utils.py
+++ b/django/contrib/staticfiles/utils.py
@@ -3,30 +3,34 @@ import fnmatch
 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)
diff --git a/docs/howto/static-files.txt b/docs/howto/static-files.txt
index 16f8ac4c11..465b5baeae 100644
--- a/docs/howto/static-files.txt
+++ b/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,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
 :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
@@ -195,30 +200,21 @@ It is also able to consume standard context variables, e.g. assuming a
 
 .. 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:
+.. note::
 
-.. 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 %}
-    <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" />
+.. _`urlparse.urljoin()`: http://docs.python.org/library/urlparse.html#urlparse.urljoin
 
 .. _staticfiles-development:
 
diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt
index 5ab3c1e02f..ae9521051e 100644
--- a/docs/ref/contrib/staticfiles.txt
+++ b/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
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index 5c08c66c21..a7d548a01c 100644
--- a/docs/ref/templates/builtins.txt
+++ b/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
diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
index ec4eae09af..02c4e82693 100644
--- a/docs/releases/1.4.txt
+++ b/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
 ~~~~~~~~~~~~~~
 
diff --git a/tests/regressiontests/staticfiles_tests/project/documents/cached/absolute.css b/tests/regressiontests/staticfiles_tests/project/documents/cached/absolute.css
new file mode 100644
index 0000000000..e64e7ccca7
--- /dev/null
+++ b/tests/regressiontests/staticfiles_tests/project/documents/cached/absolute.css
@@ -0,0 +1 @@
+@import url("/static/cached/styles.css");
diff --git a/tests/regressiontests/staticfiles_tests/project/documents/cached/denorm.css b/tests/regressiontests/staticfiles_tests/project/documents/cached/denorm.css
new file mode 100644
index 0000000000..27b9a349b0
--- /dev/null
+++ b/tests/regressiontests/staticfiles_tests/project/documents/cached/denorm.css
@@ -0,0 +1 @@
+@import url("..//cached///styles.css");
diff --git a/tests/regressiontests/staticfiles_tests/project/documents/cached/other.css b/tests/regressiontests/staticfiles_tests/project/documents/cached/other.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/regressiontests/staticfiles_tests/project/documents/cached/relative.css b/tests/regressiontests/staticfiles_tests/project/documents/cached/relative.css
new file mode 100644
index 0000000000..40c4a256aa
--- /dev/null
+++ b/tests/regressiontests/staticfiles_tests/project/documents/cached/relative.css
@@ -0,0 +1,2 @@
+@import url("../cached/styles.css");
+@import url("absolute.css");
\ No newline at end of file
diff --git a/tests/regressiontests/staticfiles_tests/project/documents/cached/styles.css b/tests/regressiontests/staticfiles_tests/project/documents/cached/styles.css
new file mode 100644
index 0000000000..84936d1dcb
--- /dev/null
+++ b/tests/regressiontests/staticfiles_tests/project/documents/cached/styles.css
@@ -0,0 +1 @@
+@import url("cached/other.css");
\ No newline at end of file
diff --git a/tests/regressiontests/staticfiles_tests/project/documents/cached/url.css b/tests/regressiontests/staticfiles_tests/project/documents/cached/url.css
new file mode 100644
index 0000000000..184e254004
--- /dev/null
+++ b/tests/regressiontests/staticfiles_tests/project/documents/cached/url.css
@@ -0,0 +1 @@
+@import url("https://www.djangoproject.com/m/css/base.css");
\ No newline at end of file
diff --git a/tests/regressiontests/staticfiles_tests/project/site_media/static/testfile.txt b/tests/regressiontests/staticfiles_tests/project/site_media/static/testfile.txt
new file mode 100644
index 0000000000..4d92dbe1ad
--- /dev/null
+++ b/tests/regressiontests/staticfiles_tests/project/site_media/static/testfile.txt
@@ -0,0 +1 @@
+Test!
\ No newline at end of file
diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py
index 77b771d23d..40569484df 100644
--- a/tests/regressiontests/staticfiles_tests/tests.py
+++ b/tests/regressiontests/staticfiles_tests/tests.py
@@ -8,6 +8,7 @@ import sys
 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.utils._os import rmtree_errorhandler
 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 @@ class StaticFilesTestCase(TestCase):
         # 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,27 +66,26 @@ class StaticFilesTestCase(TestCase):
     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
@@ -76,7 +93,7 @@ class BuildStaticTestCase(StaticFilesTestCase):
     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 @@ class BuildStaticTestCase(StaticFilesTestCase):
 
     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 @@ class BuildStaticTestCase(StaticFilesTestCase):
             return f.read()
 
 
+class CollectionTestCase(BaseCollectionTestCase, StaticFilesTestCase):
+    pass
+
+
 class TestDefaults(object):
     """
     A few standard test cases.
@@ -142,7 +163,7 @@ class TestDefaults(object):
         self.assertFileContains(u'test/camelCase.txt', u'camelCase')
 
 
-class TestFindStatic(BuildStaticTestCase, TestDefaults):
+class TestFindStatic(CollectionTestCase, TestDefaults):
     """
     Test ``findstatic`` management command.
     """
@@ -171,12 +192,12 @@ class TestFindStatic(BuildStaticTestCase, TestDefaults):
             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,7 +216,7 @@ class TestBuildStatic(BuildStaticTestCase, TestDefaults):
         self.assertFileNotFound('test/CVS')
 
 
-class TestBuildStaticClear(BuildStaticTestCase):
+class TestCollectionClear(CollectionTestCase):
     """
     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')
         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 @@ class TestNoFilesCreated(object):
         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 @@ if sys.platform != 'win32':
         ``--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 @@ class TestMiscFinder(TestCase):
             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)
 
-TestStaticfilesDirsType = override_settings(
-    STATICFILES_DIRS = 'a string',
-)(TestStaticfilesDirsType)
+
+class TestTemplateTag(StaticFilesTestCase):
+
+    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")