Ticket #15252: 15252.4.diff

File 15252.4.diff, 60.3 KB (added by Jannis Leidel, 13 years ago)
  • django/contrib/admin/helpers.py

    diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
    index d15716b..04a3492 100644
    a b  
    11from django import forms
    22from django.contrib.admin.util import (flatten_fieldsets, lookup_field,
    33    display_for_field, label_for_field, help_text_for_field)
     4from django.contrib.admin.templatetags.admin_static import static
    45from django.contrib.contenttypes.models import ContentType
    56from django.core.exceptions import ObjectDoesNotExist
    67from django.db.models.fields.related import ManyToManyRel
    class Fieldset(object):  
    7576    def _media(self):
    7677        if 'collapse' in self.classes:
    7778            js = ['jquery.min.js', 'jquery.init.js', 'collapse.min.js']
    78             return forms.Media(js=['admin/js/%s' % url for url in js])
     79            return forms.Media(js=[static('admin/js/%s' % url) for url in js])
    7980        return forms.Media()
    8081    media = property(_media)
    8182
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index 35c3cde..81e8ae5 100644
    a b from django.forms.models import (modelform_factory, modelformset_factory,  
    66from django.contrib.contenttypes.models import ContentType
    77from django.contrib.admin import widgets, helpers
    88from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
     9from django.contrib.admin.templatetags.admin_static import static
    910from django.contrib import messages
    1011from django.views.decorators.csrf import csrf_protect
    1112from django.core.exceptions import PermissionDenied, ValidationError
    class ModelAdmin(BaseModelAdmin):  
    350351        return self.get_urls()
    351352    urls = property(urls)
    352353
    353     def _media(self):
     354    @property
     355    def media(self):
    354356        js = [
    355357            'core.js',
    356358            'admin/RelatedObjectLookups.js',
    class ModelAdmin(BaseModelAdmin):  
    363365            js.extend(['urlify.js', 'prepopulate.min.js'])
    364366        if self.opts.get_ordered_objects():
    365367            js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.js'])
    366         return forms.Media(js=['admin/js/%s' % url for url in js])
    367     media = property(_media)
     368        return forms.Media(js=[static('admin/js/%s' % url) for url in js])
    368369
    369370    def has_add_permission(self, request):
    370371        """
    class InlineModelAdmin(BaseModelAdmin):  
    13221323        if self.verbose_name_plural is None:
    13231324            self.verbose_name_plural = self.model._meta.verbose_name_plural
    13241325
    1325     def _media(self):
     1326    @property
     1327    def media(self):
    13261328        js = ['jquery.min.js', 'jquery.init.js', 'inlines.min.js']
    13271329        if self.prepopulated_fields:
    13281330            js.extend(['urlify.js', 'prepopulate.min.js'])
    13291331        if self.filter_vertical or self.filter_horizontal:
    13301332            js.extend(['SelectBox.js', 'SelectFilter2.js'])
    1331         return forms.Media(js=['admin/js/%s' % url for url in js])
    1332     media = property(_media)
     1333        return forms.Media(js=[static('admin/js/%s' % url) for url in js])
    13331334
    13341335    def get_formset(self, request, obj=None, **kwargs):
    13351336        """Returns a BaseInlineFormSet class for use in admin add/change views."""
  • django/contrib/admin/templates/admin/auth/user/change_password.html

    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 c280f50..b18b0aa 100644
    a b  
    11{% extends "admin/base_site.html" %}
    2 {% load i18n static admin_modify %}
     2{% load i18n admin_static admin_modify %}
    33{% load url from future %}
    44{% block extrahead %}{{ block.super }}
    55{% url 'admin:jsi18n' as jsi18nurl %}
  • django/contrib/admin/templates/admin/base.html

    diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html
    index 9282440..4b3c429 100644
    a b  
    1 {% load static %}{% load url from future %}<!DOCTYPE html>
     1{% load admin_static %}{% load url from future %}<!DOCTYPE html>
    22<html lang="{{ LANGUAGE_CODE|default:"en-us" }}" {% if LANGUAGE_BIDI %}dir="rtl"{% endif %}>
    33<head>
    44<title>{% block title %}{% endblock %}</title>
  • django/contrib/admin/templates/admin/change_form.html

    diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
    index c5e428d..56661e9 100644
    a b  
    11{% extends "admin/base_site.html" %}
    2 {% load i18n static admin_modify %}
     2{% load i18n admin_static admin_modify %}
    33{% load url from future %}
    44
    55{% block extrahead %}{{ block.super }}
  • django/contrib/admin/templates/admin/change_list.html

    diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
    index 29af47f..24c6d8c 100644
    a b  
    11{% extends "admin/base_site.html" %}
    2 {% load i18n static admin_list %}
     2{% load i18n admin_static admin_list %}
    33{% load url from future %}
    44{% block extrastyle %}
    55  {{ block.super }}
  • django/contrib/admin/templates/admin/change_list_results.html

    diff --git a/django/contrib/admin/templates/admin/change_list_results.html b/django/contrib/admin/templates/admin/change_list_results.html
    index 233d4e1..b1db647 100644
    a b  
    1 {% load i18n static %}
     1{% load i18n admin_static %}
    22{% if result_hidden_fields %}
    33<div class="hiddenfields">{# DIV for HTML validation #}
    44{% for item in result_hidden_fields %}{{ item }}{% endfor %}
  • django/contrib/admin/templates/admin/edit_inline/stacked.html

    diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html
    index 7e69450..476e261 100644
    a b  
    1 {% load i18n static %}
     1{% load i18n admin_static %}
    22<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
    33  <h2>{{ inline_admin_formset.opts.verbose_name_plural|title }}</h2>
    44{{ inline_admin_formset.formset.management_form }}
  • django/contrib/admin/templates/admin/edit_inline/tabular.html

    diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
    index 8294227..29db95a 100644
    a b  
    1 {% load i18n static admin_modify %}
     1{% load i18n admin_static admin_modify %}
    22<div class="inline-group" id="{{ inline_admin_formset.formset.prefix }}-group">
    33  <div class="tabular inline-related {% if forloop.last %}last-related{% endif %}">
    44{{ inline_admin_formset.formset.management_form }}
  • django/contrib/admin/templates/admin/index.html

    diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html
    index 0f81a1a..7164220 100644
    a b  
    11{% extends "admin/base_site.html" %}
    2 {% load i18n static %}
     2{% load i18n admin_static %}
    33
    44{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}" />{% endblock %}
    55
  • django/contrib/admin/templates/admin/login.html

    diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html
    index a95cfd8..dbaa119 100644
    a b  
    11{% extends "admin/base_site.html" %}
    2 {% load i18n static %}
     2{% load i18n admin_static %}
    33
    44{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/login.css" %}" />{% endblock %}
    55
  • django/contrib/admin/templates/admin/search_form.html

    diff --git a/django/contrib/admin/templates/admin/search_form.html b/django/contrib/admin/templates/admin/search_form.html
    index 2b23a21..162b54a 100644
    a b  
    1 {% load i18n static %}
     1{% load i18n admin_static %}
    22{% if cl.search_fields %}
    33<div id="toolbar"><form id="changelist-search" action="" method="get">
    44<div><!-- DIV needed for valid HTML -->
  • django/contrib/admin/templatetags/admin_list.py

    diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
    index 7a3a9b8..0f5eafc 100644
    a b import datetime  
    33from django.contrib.admin.util import lookup_field, display_for_field, label_for_field
    44from django.contrib.admin.views.main import (ALL_VAR, EMPTY_CHANGELIST_VALUE,
    55    ORDER_VAR, PAGE_VAR, SEARCH_VAR)
     6from django.contrib.admin.templatetags.admin_static import static
    67from django.core.exceptions import ObjectDoesNotExist
    78from django.db import models
    8 from django.templatetags.static import static
    99from django.utils import formats
    1010from django.utils.html import escape, conditional_escape
    1111from django.utils.safestring import mark_safe
  • new file django/contrib/admin/templatetags/admin_static.py

    diff --git a/django/contrib/admin/templatetags/admin_static.py b/django/contrib/admin/templatetags/admin_static.py
    new file mode 100644
    index 0000000..5ea3ba5
    - +  
     1from django.conf import settings
     2from django.template import Library
     3
     4register = Library()
     5
     6if 'django.contrib.staticfiles' in settings.INSTALLED_APPS:
     7    from django.contrib.staticfiles.templatetags.staticfiles import static
     8else:
     9    from django.templatetags.static import static
     10
     11static = register.simple_tag(static)
  • django/contrib/admin/widgets.py

    diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
    index 038351e..0d1f2a9 100644
    a b Form Widget classes specific to the Django admin site.  
    44
    55import copy
    66from django import forms
     7from django.contrib.admin.templatetags.admin_static import static
    78from django.core.urlresolvers import reverse
    89from django.forms.widgets import RadioFieldRenderer
    910from django.forms.util import flatatt
    10 from django.templatetags.static import static
    1111from django.utils.html import escape
    1212from django.utils.text import Truncator
    1313from django.utils.translation import ugettext as _
    1414from django.utils.safestring import mark_safe
    1515from django.utils.encoding import force_unicode
    1616
     17
    1718class FilteredSelectMultiple(forms.SelectMultiple):
    1819    """
    1920    A SelectMultiple with a JavaScript filter interface.
    class FilteredSelectMultiple(forms.SelectMultiple):  
    2122    Note that the resulting JavaScript assumes that the jsi18n
    2223    catalog has been loaded in the page
    2324    """
    24     class Media:
    25         js = ["admin/js/%s" % path
    26               for path in ["core.js", "SelectBox.js", "SelectFilter2.js"]]
     25    @property
     26    def media(self):
     27        js = ["core.js", "SelectBox.js", "SelectFilter2.js"]
     28        return forms.Media(js=[static("admin/js/%s" % path) for path in js])
    2729
    2830    def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
    2931        self.verbose_name = verbose_name
    class FilteredSelectMultiple(forms.SelectMultiple):  
    3133        super(FilteredSelectMultiple, self).__init__(attrs, choices)
    3234
    3335    def render(self, name, value, attrs=None, choices=()):
    34         if attrs is None: attrs = {}
     36        if attrs is None:
     37            attrs = {}
    3538        attrs['class'] = 'selectfilter'
    36         if self.is_stacked: attrs['class'] += 'stacked'
     39        if self.is_stacked:
     40            attrs['class'] += 'stacked'
    3741        output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)]
    3842        output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
    3943        # TODO: "id_" is hard-coded here. This should instead use the correct
    class FilteredSelectMultiple(forms.SelectMultiple):  
    4347        return mark_safe(u''.join(output))
    4448
    4549class AdminDateWidget(forms.DateInput):
    46     class Media:
    47         js = ["admin/js/calendar.js", "admin/js/admin/DateTimeShortcuts.js"]
     50
     51    @property
     52    def media(self):
     53        js = ["calendar.js", "admin/DateTimeShortcuts.js"]
     54        return forms.Media(js=[static("admin/js/%s" % path) for path in js])
    4855
    4956    def __init__(self, attrs={}, format=None):
    5057        super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'}, format=format)
    5158
    5259class AdminTimeWidget(forms.TimeInput):
    53     class Media:
    54         js = ["admin/js/calendar.js", "admin/js/admin/DateTimeShortcuts.js"]
     60
     61    @property
     62    def media(self):
     63        js = ["calendar.js", "admin/DateTimeShortcuts.js"]
     64        return forms.Media(js=[static("admin/js/%s" % path) for path in js])
    5565
    5666    def __init__(self, attrs={}, format=None):
    5767        super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'}, format=format)
    class RelatedFieldWidgetWrapper(forms.Widget):  
    232242        memo[id(self)] = obj
    233243        return obj
    234244
    235     def _media(self):
     245    @property
     246    def media(self):
    236247        return self.widget.media
    237     media = property(_media)
    238248
    239249    def render(self, name, value, *args, **kwargs):
    240250        rel_to = self.rel.to
  • django/contrib/staticfiles/finders.py

    diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py
    index ffb96e0..45bf4a1 100644
    a b class BaseFinder(object):  
    2828        """
    2929        raise NotImplementedError()
    3030
    31     def list(self, ignore_patterns=[]):
     31    def list(self, ignore_patterns):
    3232        """
    3333        Given an optional list of paths to ignore, this should return
    3434        a two item iterable consisting of the relative path and storage
  • django/contrib/staticfiles/management/commands/collectstatic.py

    diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py
    index dd597ad..6ceddca 100644
    a b import os  
    44import sys
    55from optparse import make_option
    66
    7 from django.conf import settings
    8 from django.core.files.storage import FileSystemStorage, get_storage_class
     7from django.core.files.storage import FileSystemStorage
    98from django.core.management.base import CommandError, NoArgsCommand
    109from django.utils.encoding import smart_str, smart_unicode
    1110
    12 from django.contrib.staticfiles import finders
     11from django.contrib.staticfiles import finders, storage
    1312
    1413
    1514class Command(NoArgsCommand):
    class Command(NoArgsCommand):  
    1817    locations to the settings.STATIC_ROOT.
    1918    """
    2019    option_list = NoArgsCommand.option_list + (
    21         make_option('--noinput', action='store_false', dest='interactive',
    22             default=True, help="Do NOT prompt the user for input of any kind."),
     20        make_option('--noinput',
     21            action='store_false', dest='interactive', default=True,
     22            help="Do NOT prompt the user for input of any kind."),
    2323        make_option('-i', '--ignore', action='append', default=[],
    2424            dest='ignore_patterns', metavar='PATTERN',
    2525            help="Ignore files or directories matching this glob-style "
    2626                "pattern. Use multiple times to ignore more."),
    27         make_option('-n', '--dry-run', action='store_true', dest='dry_run',
    28             default=False, help="Do everything except modify the filesystem."),
    29         make_option('-c', '--clear', action='store_true', dest='clear',
    30             default=False, help="Clear the existing files using the storage "
    31                 "before trying to copy or link the original file."),
    32         make_option('-l', '--link', action='store_true', dest='link',
    33             default=False, help="Create a symbolic link to each file instead of copying."),
     27        make_option('-n', '--dry-run',
     28            action='store_true', dest='dry_run', default=False,
     29            help="Do everything except modify the filesystem."),
     30        make_option('-c', '--clear',
     31            action='store_true', dest='clear', default=False,
     32            help="Clear the existing files using the storage "
     33                 "before trying to copy or link the original file."),
     34        make_option('-l', '--link',
     35            action='store_true', dest='link', default=False,
     36            help="Create a symbolic link to each file instead of copying."),
    3437        make_option('--no-default-ignore', action='store_false',
    3538            dest='use_default_ignore_patterns', default=True,
    3639            help="Don't ignore the common private glob-style patterns 'CVS', "
    3740                "'.*' and '*~'."),
    3841    )
    39     help = "Collect static files from apps and other locations in a single location."
     42    help = "Collect static files in a single location."
    4043
    4144    def __init__(self, *args, **kwargs):
    4245        super(NoArgsCommand, self).__init__(*args, **kwargs)
    4346        self.copied_files = []
    4447        self.symlinked_files = []
    4548        self.unmodified_files = []
    46         self.storage = get_storage_class(settings.STATICFILES_STORAGE)()
     49        self.storage = storage.staticfiles_storage
    4750        try:
    4851            self.storage.path('')
    4952        except NotImplementedError:
    Type 'yes' to continue, or 'no' to cancel: """  
    104107
    105108        handler = {
    106109            True: self.link_file,
    107             False: self.copy_file
     110            False: self.copy_file,
    108111        }[self.symlink]
    109112
     113        found_files = []
    110114        for finder in finders.get_finders():
    111115            for path, storage in finder.list(self.ignore_patterns):
    112116                # Prefix the relative path if the source storage contains it
    Type 'yes' to continue, or 'no' to cancel: """  
    114118                    prefixed_path = os.path.join(storage.prefix, path)
    115119                else:
    116120                    prefixed_path = path
     121                found_files.append(prefixed_path)
    117122                handler(path, prefixed_path, storage)
    118123
    119         actual_count = len(self.copied_files) + len(self.symlinked_files)
     124        # Here we check if the storage backend has a post_process
     125        # method and pass it the list of modified files.
     126        if hasattr(self.storage, 'post_process'):
     127            post_processed = self.storage.post_process(found_files, **options)
     128            for path in post_processed:
     129                self.log(u"Post-processed '%s'" % path, level=1)
     130        else:
     131            post_processed = []
     132
     133        modified_files = self.copied_files + self.symlinked_files
     134        actual_count = len(modified_files)
    120135        unmodified_count = len(self.unmodified_files)
     136
    121137        if self.verbosity >= 1:
    122             self.stdout.write(smart_str(u"\n%s static file%s %s %s%s.\n"
    123                               % (actual_count,
    124                                  actual_count != 1 and 's' or '',
    125                                  self.symlink and 'symlinked' or 'copied',
    126                                  destination_path and "to '%s'"
    127                                     % destination_path or '',
    128                                  unmodified_count and ' (%s unmodified)'
    129                                     % unmodified_count or '')))
     138            template = ("\n%(actual_count)s %(identifier)s %(action)s"
     139                        "%(destination)s%(unmodified)s.\n")
     140            summary = template % {
     141                'actual_count': actual_count,
     142                'identifier': 'static file' + (actual_count > 1 and 's' or ''),
     143                'action': self.symlink and 'symlinked' or 'copied',
     144                'destination': (destination_path and " to '%s'"
     145                                % destination_path or ''),
     146                'unmodified': (self.unmodified_files and ', %s unmodified'
     147                               % unmodified_count or ''),
     148            }
     149            self.stdout.write(smart_str(summary))
    130150
    131151    def log(self, msg, level=2):
    132152        """
    Type 'yes' to continue, or 'no' to cancel: """  
    146166        for f in files:
    147167            fpath = os.path.join(path, f)
    148168            if self.dry_run:
    149                 self.log(u"Pretending to delete '%s'" % smart_unicode(fpath), level=1)
     169                self.log(u"Pretending to delete '%s'" %
     170                         smart_unicode(fpath), level=1)
    150171            else:
    151172                self.log(u"Deleting '%s'" % smart_unicode(fpath), level=1)
    152173                self.storage.delete(fpath)
    Type 'yes' to continue, or 'no' to cancel: """  
    159180        if self.storage.exists(prefixed_path):
    160181            try:
    161182                # When was the target file modified last time?
    162                 target_last_modified = self.storage.modified_time(prefixed_path)
     183                target_last_modified = \
     184                    self.storage.modified_time(prefixed_path)
    163185            except (OSError, NotImplementedError):
    164186                # The storage doesn't support ``modified_time`` or failed
    165187                pass
    Type 'yes' to continue, or 'no' to cancel: """  
    177199                        full_path = None
    178200                    # Skip the file if the source file is younger
    179201                    if target_last_modified >= source_last_modified:
    180                         if not ((self.symlink and full_path and not os.path.islink(full_path)) or
    181                                 (not self.symlink and full_path and os.path.islink(full_path))):
     202                        if not ((self.symlink and full_path
     203                                 and not os.path.islink(full_path)) or
     204                                (not self.symlink and full_path
     205                                 and os.path.islink(full_path))):
    182206                            if prefixed_path not in self.unmodified_files:
    183207                                self.unmodified_files.append(prefixed_path)
    184208                            self.log(u"Skipping '%s' (not modified)" % path)
  • django/contrib/staticfiles/storage.py

    diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py
    index a69ae23..960f581 100644
    a b  
     1import hashlib
    12import os
     3import posixpath
     4import re
     5
    26from django.conf import settings
     7from django.core.cache import (get_cache, InvalidCacheBackendError,
     8                               cache as default_cache)
    39from django.core.exceptions import ImproperlyConfigured
    4 from django.core.files.storage import FileSystemStorage
     10from django.core.files.base import ContentFile
     11from django.core.files.storage import FileSystemStorage, get_storage_class
     12from django.utils.encoding import force_unicode
     13from django.utils.functional import LazyObject
    514from django.utils.importlib import import_module
     15from django.utils.datastructures import SortedDict
    616
    7 from django.contrib.staticfiles import utils
     17from django.contrib.staticfiles.utils import check_settings, matches_patterns
    818
    919
    1020class StaticFilesStorage(FileSystemStorage):
    class StaticFilesStorage(FileSystemStorage):  
    2636        if base_url is None:
    2737            raise ImproperlyConfigured("You're using the staticfiles app "
    2838                "without having set the STATIC_URL setting.")
    29         utils.check_settings()
    30         super(StaticFilesStorage, self).__init__(location, base_url, *args, **kwargs)
     39        check_settings()
     40        super(StaticFilesStorage, self).__init__(location, base_url,
     41                                                 *args, **kwargs)
     42
     43
     44class CachedFilesMixin(object):
     45    patterns = (
     46        ("*.css", (
     47            r"""(url\(['"]{0,1}\s*(.*?)["']{0,1}\))""",
     48            r"""(@import\s*["']\s*(.*?)["'])""",
     49        )),
     50    )
     51
     52    def __init__(self, *args, **kwargs):
     53        super(CachedFilesMixin, self).__init__(*args, **kwargs)
     54        try:
     55            self.cache = get_cache('staticfiles')
     56        except InvalidCacheBackendError:
     57            # Use the default backend
     58            self.cache = default_cache
     59        self._patterns = SortedDict()
     60        for extension, patterns in self.patterns:
     61            for pattern in patterns:
     62                compiled = re.compile(pattern)
     63                self._patterns.setdefault(extension, []).append(compiled)
     64
     65    def hashed_name(self, name, content=None):
     66        if content is None:
     67            if not self.exists(name):
     68                raise ValueError("The file '%s' could not be found with %r." %
     69                                 (name, self))
     70            try:
     71                content = self.open(name)
     72            except IOError:
     73                # Handle directory paths
     74                return name
     75        path, filename = os.path.split(name)
     76        root, ext = os.path.splitext(filename)
     77        # Get the MD5 hash of the file
     78        md5 = hashlib.md5()
     79        for chunk in content.chunks():
     80            md5.update(chunk)
     81        md5sum = md5.hexdigest()[:12]
     82        return os.path.join(path, u"%s.%s%s" % (root, md5sum, ext))
     83
     84    def cache_key(self, name):
     85        return u'staticfiles:cache:%s' % name
     86
     87    def url(self, name, force=False):
     88        """
     89        Returns the real URL in DEBUG mode.
     90        """
     91        if settings.DEBUG and not force:
     92            return super(CachedFilesMixin, self).url(name)
     93        cache_key = self.cache_key(name)
     94        hashed_name = self.cache.get(cache_key)
     95        if hashed_name is None:
     96            hashed_name = self.hashed_name(name)
     97        return super(CachedFilesMixin, self).url(hashed_name)
     98
     99    def url_converter(self, name):
     100        """
     101        Returns the custom URL converter for the given file name.
     102        """
     103        def converter(matchobj):
     104            """
     105            Converts the matched URL depending on the parent level (`..`)
     106            and returns the normalized and hashed URL using the url method
     107            of the storage.
     108            """
     109            matched, url = matchobj.groups()
     110            # Completely ignore http(s) prefixed URLs
     111            if url.startswith(('http', 'https')):
     112                return matched
     113            name_parts = name.split('/')
     114            # Using posix normpath here to remove duplicates
     115            result = url_parts = posixpath.normpath(url).split('/')
     116            level = url.count('..')
     117            if level:
     118                result = name_parts[:-level - 1] + url_parts[level:]
     119            elif name_parts[:-1]:
     120                result = name_parts[:-1] + url_parts[-1:]
     121            joined_result = '/'.join(result)
     122            hashed_url = self.url(joined_result, force=True)
     123            # Return the hashed and normalized version to the file
     124            return 'url("%s")' % hashed_url
     125        return converter
     126
     127    def post_process(self, paths, dry_run=False, **options):
     128        """
     129        Post process the given list of files (called from collectstatic).
     130        """
     131        processed_files = []
     132        # don't even dare to process the files if we're in dry run mode
     133        if dry_run:
     134            return processed_files
     135
     136        # delete cache of all handled paths
     137        self.cache.delete_many([self.cache_key(path) for path in paths])
     138
     139        # only try processing the files we have patterns for
     140        matches = lambda path: matches_patterns(path, self._patterns.keys())
     141        processing_paths = [path for path in paths if matches(path)]
     142
     143        # then sort the files by the directory level
     144        path_level = lambda name: len(name.split('/'))
     145        for name in sorted(paths, key=path_level, reverse=True):
     146            with self.open(name) as original_file:
     147
     148                # first get the original's file content
     149                content = original_file.read()
     150
     151                # and a hashed name for it
     152                hashed_name = self.hashed_name(name, ContentFile(content))
     153
     154                # then apply each replacement pattern on the content
     155                if name in processing_paths:
     156                    converter = self.url_converter(name)
     157                    for patterns in self._patterns.values():
     158                        for pattern in patterns:
     159                            content = pattern.sub(converter, content)
     160
     161                # and save the result
     162                if self.exists(hashed_name):
     163                    self.delete(hashed_name)
     164                saved_name = self._save(hashed_name, ContentFile(content))
     165                hashed_name = force_unicode(saved_name.replace('\\', '/'))
     166                processed_files.append(hashed_name)
     167                self.cache.set(self.cache_key(name), hashed_name)
     168
     169        return processed_files
     170
     171class CachedStaticFilesStorage(CachedFilesMixin, StaticFilesStorage):
     172    """
     173    A static file system storage backend which also saves
     174    hashed copies of the files it saves.
     175    """
     176    pass
    31177
    32178
    33179class AppStaticStorage(FileSystemStorage):
    class AppStaticStorage(FileSystemStorage):  
    47193        mod_path = os.path.dirname(mod.__file__)
    48194        location = os.path.join(mod_path, self.source_dir)
    49195        super(AppStaticStorage, self).__init__(location, *args, **kwargs)
     196
     197
     198class ConfiguredStorage(LazyObject):
     199    def _setup(self):
     200        self._wrapped = get_storage_class(settings.STATICFILES_STORAGE)()
     201
     202staticfiles_storage = ConfiguredStorage()
  • new file django/contrib/staticfiles/templatetags/staticfiles.py

    diff --git a/django/contrib/staticfiles/templatetags/__init__.py b/django/contrib/staticfiles/templatetags/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/django/contrib/staticfiles/templatetags/staticfiles.py b/django/contrib/staticfiles/templatetags/staticfiles.py
    new file mode 100644
    index 0000000..788f06e
    - +  
     1from django import template
     2from django.contrib.staticfiles.storage import staticfiles_storage
     3
     4register = template.Library()
     5
     6
     7@register.simple_tag
     8def static(path):
     9    """
     10    A template tag that returns the URL to a file
     11    using staticfiles' storage backend
     12    """
     13    return staticfiles_storage.url(path)
  • django/contrib/staticfiles/utils.py

    diff --git a/django/contrib/staticfiles/utils.py b/django/contrib/staticfiles/utils.py
    index 9ff4bc4..c175eda 100644
    a b import fnmatch  
    33from django.conf import settings
    44from django.core.exceptions import ImproperlyConfigured
    55
    6 def is_ignored(path, ignore_patterns=[]):
     6def matches_patterns(path, ignore_patterns=[]):
    77    """
    88    Return True or False depending on whether the ``path`` should be
    99    ignored (if it matches any pattern in ``ignore_patterns``).
    def is_ignored(path, ignore_patterns=[]):  
    1313            return True
    1414    return False
    1515
    16 def get_files(storage, ignore_patterns=[], location=''):
     16def get_files(storage, ignore_patterns=None, location=''):
    1717    """
    1818    Recursively walk the storage directories yielding the paths
    1919    of all files that should be copied.
    2020    """
     21    if ignore_patterns is None:
     22        ignore_patterns = []
    2123    directories, files = storage.listdir(location)
    2224    for fn in files:
    23         if is_ignored(fn, ignore_patterns):
     25        if matches_patterns(fn, ignore_patterns):
    2426            continue
    2527        if location:
    2628            fn = os.path.join(location, fn)
    2729        yield fn
    2830    for dir in directories:
    29         if is_ignored(dir, ignore_patterns):
     31        if matches_patterns(dir, ignore_patterns):
    3032            continue
    3133        if location:
    3234            dir = os.path.join(location, dir)
  • docs/howto/static-files.txt

    diff --git a/docs/howto/static-files.txt b/docs/howto/static-files.txt
    index 16f8ac4..465b5ba 100644
    a b Basic usage  
    7070
    7171       <img src="{{ STATIC_URL }}images/hi.jpg" />
    7272
    73    See :ref:`staticfiles-in-templates` for more details, including an
     73   See :ref:`staticfiles-in-templates` for more details, **including** an
    7474   alternate method using a template tag.
    7575
    7676Deploying static files in a nutshell
    A far better way is to use the value of the :setting:`STATIC_URL` setting  
    143143directly in your templates. This means that a switch of static files servers
    144144only requires changing that single value. Much better!
    145145
    146 ``staticfiles`` includes two built-in ways of getting at this setting in your
     146Django includes multiple built-in ways of using this setting in your
    147147templates: a context processor and a template tag.
    148148
    149149With a context processor
    but in views written by hand you'll need to explicitly use ``RequestContext``  
    180180To see how that works, and to read more details, check out
    181181:ref:`subclassing-context-requestcontext`.
    182182
     183Another option is the :ttag:`get_static_prefix` template tag that is part of
     184Django's core.
     185
    183186With a template tag
    184187-------------------
    185188
    186 To easily link to static files Django ships with a :ttag:`static` template tag.
     189The more powerful tool is the :ttag:`static<staticfiles-static>` template
     190tag. It builds the URL for the given relative path by using the configured
     191:setting:`STATICFILES_STORAGE` storage.
    187192
    188193.. code-block:: html+django
    189194
    190     {% load static %}
     195    {% load staticfiles %}
    191196    <img src="{% static "images/hi.jpg" %}" />
    192197
    193198It 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  
    195200
    196201.. code-block:: html+django
    197202
    198     {% load static %}
     203    {% load staticfiles %}
    199204    <link rel="stylesheet" href="{% static user_stylesheet %}" type="text/css" media="screen" />
    200205
    201 Another option is the :ttag:`get_static_prefix` template tag. You can use
    202 this if you're not using :class:`~django.template.RequestContext` (and
    203 therefore not relying on the ``django.core.context_processors.static``
    204 context processor), or if you need more control over exactly where and how
    205 :setting:`STATIC_URL` is injected into the template. Here's an example:
    206 
    207 .. code-block:: html+django
    208 
    209     {% load static %}
    210     <img src="{% get_static_prefix %}images/hi.jpg" />
    211 
    212 There's also a second form you can use to avoid extra processing if you need
    213 the value multiple times:
    214 
    215 .. code-block:: html+django
     206.. note::
    216207
    217     {% load static %}
    218     {% get_static_prefix as STATIC_PREFIX %}
     208    There is also a template tag named :ttag:`static` in Django's core set
     209    of :ref:`built in template tags<ref-templates-builtins-tags>` which has
     210    the same argument signature but only uses `urlparse.urljoin()`_ with the
     211    :setting:`STATIC_URL` setting and the given path. This has the
     212    disadvantage of not being able to easily switch the storage backend
     213    without changing the templates, so in doubt use the ``staticfiles``
     214    :ttag:`static<staticfiles-static>`
     215    template tag.
    219216
    220     <img src="{{ STATIC_PREFIX }}images/hi.jpg" />
    221     <img src="{{ STATIC_PREFIX }}images/hi2.jpg" />
     217.. _`urlparse.urljoin()`: http://docs.python.org/library/urlparse.html#urlparse.urljoin
    222218
    223219.. _staticfiles-development:
    224220
  • docs/ref/contrib/staticfiles.txt

    diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt
    index 5ab3c1e..76b6aa7 100644
    a b in a ``'downloads'`` subdirectory of :setting:`STATIC_ROOT`.  
    6868
    6969This would allow you to refer to the local file
    7070``'/opt/webfiles/stats/polls_20101022.tar.gz'`` with
    71 ``'/static/downloads/polls_20101022.tar.gz'`` in your templates, e.g.::
     71``'/static/downloads/polls_20101022.tar.gz'`` in your templates, e.g.:
     72
     73.. code-block:: html+django
    7274
    7375    <a href="{{ STATIC_URL }}downloads/polls_20101022.tar.gz">
    7476
    Files are searched by using the :setting:`enabled finders  
    141143:setting:`STATICFILES_DIRS` and in the ``'static'`` directory of apps
    142144specified by the :setting:`INSTALLED_APPS` setting.
    143145
     146.. versionadded:: 1.4
     147
     148The :djadmin:`collectstatic` management command calls the
     149:meth:`~django.contrib.staticfiles.storage.StaticFilesStorage.post_process`
     150method of the :setting:`STATICFILES_STORAGE` after each run and passes
     151a list of paths that have been modified by the management command. It also
     152receives all command line options of :djadmin:`collectstatic`. This is used
     153by the :class:`~django.contrib.staticfiles.storage.CachedStaticFilesStorage`
     154by default.
     155
    144156Some commonly used options are:
    145157
    146158.. django-admin-option:: --noinput
    Example usage::  
    237249
    238250    django-admin.py runserver --insecure
    239251
    240 .. currentmodule:: None
     252Storages
     253========
     254
     255StaticFilesStorage
     256------------------
     257
     258.. class:: storage.StaticFilesStorage
     259
     260    A subclass of the :class:`~django.core.files.storage.FileSystemStorage`
     261    storage backend that uses the :setting:`STATIC_ROOT` setting as the base
     262    file system location and the :setting:`STATIC_URL` setting respectively
     263    as the base URL.
     264
     265    .. method:: post_process(paths, **options)
     266
     267    .. versionadded:: 1.4
     268
     269    This method is called by the :djadmin:`collectstatic` management command
     270    after each run and gets passed the paths of modified files, as well as the
     271    command line options.
     272
     273    The :class:`~django.contrib.staticfiles.storage.CachedStaticFilesStorage`
     274    uses this behind the scenes to replace the paths with their hashed
     275    counterparts and update the cache appropriately.
     276
     277CachedStaticFilesStorage
     278------------------------
     279
     280.. class:: storage.CachedStaticFilesStorage
     281
     282    .. versionadded:: 1.4
     283
     284    A subclass of the :class:`~django.contrib.staticfiles.storage.StaticFilesStorage`
     285    storage backend which caches the files it saves by appending the MD5 hash
     286    of the file's content to the filename. For example, the file
     287    ``css/styles.css`` would also be saved as ``css/styles.55e7cbb9ba48.css``.
     288
     289    The purpose of this storage is to keep serving the old files in case some
     290    pages still refer to those files, e.g. because they are cached by you or
     291    a 3rd party proxy server. Additionally, it's very helpful if you want to
     292    apply `far future Expires headers`_ to the deployed files to speed up the
     293    load time for subsequent page visits.
     294
     295    The storage backend automatically replaces the paths found in the saved
     296    files matching other saved files with the path of the cached copy (using
     297    the :meth:`~django.contrib.staticfiles.storage.StaticFilesStorage.post_process`
     298    method). The regular expressions used to find those paths
     299    (``django.contrib.staticfiles.storage.CachedStaticFilesStorage.cached_patterns``)
     300    by default cover the `@import`_ rule and `url()`_ statement of `Cascading
     301    Style Sheets`_. For example, the ``'css/styles.css'`` file with the
     302    content
     303
     304    .. code-block:: css+django
     305
     306        @import url("../admin/css/base.css");
     307
     308    would be replaced by calling the
     309    :meth:`~django.core.files.storage.Storage.url`
     310    method of the ``CachedStaticFilesStorage`` storage backend, ultimatively
     311    saving a ``'css/styles.55e7cbb9ba48.css'`` file with the following
     312    content:
     313
     314    .. code-block:: css+django
     315
     316        @import url("/static/admin/css/base.27e20196a850.css");
     317
     318    To enable the ``CachedStaticFilesStorage`` you have to make sure the
     319    following requirements are met:
     320
     321    * the :setting:`STATICFILES_STORAGE` setting is set to
     322      ``'django.contrib.staticfiles.storage.CachedStaticFilesStorage'``
     323    * the :setting:`DEBUG` setting is set to ``False``
     324    * you use the ``staticfiles`` :ttag:`static<staticfiles-static>` template
     325      tag to refer to your static files in your templates
     326    * you've collected all your static files by using the
     327      :djadmin:`collectstatic` management command
     328
     329    Since creating the MD5 hash can be a performance burden to your website
     330    during runtime, ``staticfiles`` will automatically try to cache the
     331    hashed name for each file path using Django's :doc:`caching
     332    framework</topics/cache>`. If you want to override certain options of the
     333    cache backend the storage uses, simply specify a custom entry in the
     334    :setting:`CACHES` setting named ``'staticfiles'``. It falls back to using
     335    the ``'default'`` cache backend.
     336
     337.. _`far future Expires headers`: http://developer.yahoo.com/performance/rules.html#expires
     338.. _`@import`: http://www.w3.org/TR/CSS2/cascade.html#at-import
     339.. _`url()`: http://www.w3.org/TR/CSS2/syndata.html#uri
     340.. _`Cascading Style Sheets`: http://www.w3.org/Style/CSS/
     341
     342.. currentmodule:: django.contrib.staticfiles.templatetags.staticfiles
     343
     344Template tags
     345=============
     346
     347static
     348------
     349
     350.. templatetag:: staticfiles-static
     351
     352.. versionadded:: 1.4
     353
     354Uses the configued :setting:`STATICFILES_STORAGE` storage to create the
     355full URL for the given relative path, e.g.:
     356
     357.. code-block:: html+django
     358
     359    {% load static from staticfiles %}
     360    <img src="{% static "css/base.css" %}" />
     361
     362The previous example is equal to calling the ``url`` method of an instance of
     363:setting:`STATICFILES_STORAGE` with ``"css/base.css"``. This is especially
     364useful when using a non-local storage backend to deploy files as documented
     365in :ref:`staticfiles-from-cdn`.
    241366
    242367Other Helpers
    243368=============
    files:  
    251376      with :class:`~django.template.RequestContext` contexts.
    252377
    253378    - The builtin template tag :ttag:`static` which takes a path and
    254       joins it with the the static prefix :setting:`STATIC_URL`.
     379      urljoins it with the static prefix :setting:`STATIC_URL`.
    255380
    256381    - The builtin template tag :ttag:`get_static_prefix` which populates a
    257382      template variable with the static prefix :setting:`STATIC_URL` to be
  • docs/ref/templates/builtins.txt

    diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
    index 5c08c66..a7d548a 100644
    a b static  
    23532353
    23542354.. highlight:: html+django
    23552355
    2356 To link to static files Django ships with a :ttag:`static` template tag. You
    2357 can use this regardless if you're using :class:`~django.template.RequestContext`
    2358 or not.
     2356To link to static files that are saved in :setting:`STATIC_ROOT` Django ships
     2357with a :ttag:`static` template tag. You can use this regardless if you're
     2358using :class:`~django.template.RequestContext` or not.
    23592359
    23602360.. code-block:: html+django
    23612361
    It is also able to consume standard context variables, e.g. assuming a  
    23702370    {% load static %}
    23712371    <link rel="stylesheet" href="{% static user_stylesheet %}" type="text/css" media="screen" />
    23722372
     2373.. note::
     2374
     2375    The :mod:`staticfiles<django.contrib.staticfiles>` contrib app also ships
     2376    with a :ttag:`static template tag<staticfiles-static>` which uses
     2377    ``staticfiles'`` :setting:`STATICFILES_STORAGE` to build the URL of the
     2378    given path. Use that instead if you have an advanced use case such as
     2379    :ref:`using a cloud service to serve static files<staticfiles-from-cdn>`::
     2380
     2381        {% load static from staticfiles %}
     2382        <img src="{% static "images/hi.jpg" %}" />
     2383
    23732384.. templatetag:: get_static_prefix
    23742385
    23752386get_static_prefix
  • docs/releases/1.4.txt

    diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
    index 2723b42..99f08d3 100644
    a b Additionally, it's now possible to define translatable URL patterns using  
    212212:ref:`url-internationalization` for more information about the language prefix
    213213and how to internationalize URL patterns.
    214214
     215``static`` template tag
     216~~~~~~~~~~~~~~~~~~~~~~~
     217
     218The :mod:`staticfiles<django.contrib.staticfiles>` contrib app has now a new
     219:ttag:`static template tag<staticfiles-static>` to refer to files saved with
     220the :setting:`STATICFILES_STORAGE` storage backend. It'll use the storage
     221``url`` method and therefore supports advanced features such as
     222:ref:`serving files from a cloud service<staticfiles-from-cdn>`.
     223
     224``CachedStaticFilesStorage`` storage backend
     225~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     226
     227Additional to the `static template tag`_ the
     228:mod:`staticfiles<django.contrib.staticfiles>` contrib app now has a
     229:class:`~django.contrib.staticfiles.storage.CachedStaticFilesStorage` which
     230caches the files it saves (when running the :djadmin:`collectstatic`
     231management command) by appending the MD5 hash of the file's content to the
     232filename. For example, the file ``css/styles.css`` would also be saved as
     233``css/styles.55e7cbb9ba48.css``
     234
     235See the :class:`~django.contrib.staticfiles.storage.CachedStaticFilesStorage`
     236docs for more information.
     237
    215238Minor features
    216239~~~~~~~~~~~~~~
    217240
  • new file tests/regressiontests/staticfiles_tests/project/documents/cached/absolute.css

    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 0000000..e64e7cc
    - +  
     1@import url("/static/cached/styles.css");
  • new file tests/regressiontests/staticfiles_tests/project/documents/cached/denorm.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 0000000..27b9a34
    - +  
     1@import url("..//cached///styles.css");
  • new file tests/regressiontests/staticfiles_tests/project/documents/cached/relative.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 0000000..e69de29
    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 0000000..40c4a25
    - +  
     1@import url("../cached/styles.css");
     2@import url("absolute.css");
     3 No newline at end of file
  • new file tests/regressiontests/staticfiles_tests/project/documents/cached/styles.css

    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 0000000..84936d1
    - +  
     1@import url("cached/other.css");
     2 No newline at end of file
  • new file tests/regressiontests/staticfiles_tests/project/documents/cached/url.css

    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 0000000..184e254
    - +  
     1@import url("https://www.djangoproject.com/m/css/base.css");
     2 No newline at end of file
  • new file tests/regressiontests/staticfiles_tests/project/site_media/static/testfile.txt

    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 0000000..4d92dbe
    - +  
     1Test!
     2 No newline at end of file
  • tests/regressiontests/staticfiles_tests/tests.py

    diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py
    index 77b771d..4056948 100644
    a b import sys  
    88import tempfile
    99from StringIO import StringIO
    1010
     11from django.template import loader, Context
    1112from django.conf import settings
    1213from django.core.exceptions import ImproperlyConfigured
    1314from django.core.files.storage import default_storage
    from django.utils._os import rmtree_errorhandler  
    2122from django.contrib.staticfiles import finders, storage
    2223
    2324TEST_ROOT = os.path.dirname(__file__)
     25TEST_SETTINGS = {
     26    'DEBUG': True,
     27    'MEDIA_URL': '/media/',
     28    'STATIC_URL': '/static/',
     29    'MEDIA_ROOT': os.path.join(TEST_ROOT, 'project', 'site_media', 'media'),
     30    'STATIC_ROOT': os.path.join(TEST_ROOT, 'project', 'site_media', 'static'),
     31    'STATICFILES_DIRS': (
     32        os.path.join(TEST_ROOT, 'project', 'documents'),
     33        ('prefix', os.path.join(TEST_ROOT, 'project', 'prefixed')),
     34    ),
     35    'STATICFILES_FINDERS': (
     36        'django.contrib.staticfiles.finders.FileSystemFinder',
     37        'django.contrib.staticfiles.finders.AppDirectoriesFinder',
     38        'django.contrib.staticfiles.finders.DefaultStorageFinder',
     39    ),
     40}
    2441
    2542
    26 class StaticFilesTestCase(TestCase):
     43class BaseStaticFilesTestCase(object):
    2744    """
    2845    Test case with a couple utility assertions.
    2946    """
    class StaticFilesTestCase(TestCase):  
    3249        # gets accessed (by some other test), it evaluates settings.MEDIA_ROOT,
    3350        # since we're planning on changing that we need to clear out the cache.
    3451        default_storage._wrapped = empty
     52        storage.staticfiles_storage._wrapped = empty
    3553
    3654        # To make sure SVN doesn't hangs itself with the non-ASCII characters
    3755        # during checkout, we actually create one file dynamically.
    class StaticFilesTestCase(TestCase):  
    4866    def assertFileNotFound(self, filepath):
    4967        self.assertRaises(IOError, self._get_file, filepath)
    5068
    51 StaticFilesTestCase = override_settings(
    52     DEBUG = True,
    53     MEDIA_URL = '/media/',
    54     STATIC_URL = '/static/',
    55     MEDIA_ROOT =  os.path.join(TEST_ROOT, 'project', 'site_media', 'media'),
    56     STATIC_ROOT = os.path.join(TEST_ROOT, 'project', 'site_media', 'static'),
    57     STATICFILES_DIRS = (
    58         os.path.join(TEST_ROOT, 'project', 'documents'),
    59         ('prefix', os.path.join(TEST_ROOT, 'project', 'prefixed')),
    60     ),
    61     STATICFILES_FINDERS = (
    62         'django.contrib.staticfiles.finders.FileSystemFinder',
    63         'django.contrib.staticfiles.finders.AppDirectoriesFinder',
    64         'django.contrib.staticfiles.finders.DefaultStorageFinder',
    65     ),
    66 )(StaticFilesTestCase)
     69    def render_template(self, template, **kwargs):
     70        if isinstance(template, basestring):
     71            template = loader.get_template_from_string(template)
     72        return template.render(Context(kwargs)).strip()
     73
     74    def assertTemplateRenders(self, template, result, **kwargs):
     75        self.assertEqual(self.render_template(template, **kwargs), result)
    6776
     77    def assertTemplateRaises(self, exc, template, result, **kwargs):
     78        self.assertRaises(exc, self.assertTemplateRenders, template, result, **kwargs)
    6879
    69 class BuildStaticTestCase(StaticFilesTestCase):
     80
     81class StaticFilesTestCase(BaseStaticFilesTestCase, TestCase):
     82    pass
     83StaticFilesTestCase = override_settings(**TEST_SETTINGS)(StaticFilesTestCase)
     84
     85
     86class BaseCollectionTestCase(BaseStaticFilesTestCase):
    7087    """
    71     Tests shared by all file-resolving features (collectstatic,
     88    Tests shared by all file finding features (collectstatic,
    7289    findstatic, and static serve view).
    7390
    7491    This relies on the asserts defined in UtilityAssertsTestCase, but
    class BuildStaticTestCase(StaticFilesTestCase):  
    7693    all these tests.
    7794    """
    7895    def setUp(self):
    79         super(BuildStaticTestCase, self).setUp()
     96        super(BaseCollectionTestCase, self).setUp()
    8097        self.old_root = settings.STATIC_ROOT
    8198        settings.STATIC_ROOT = tempfile.mkdtemp()
    8299        self.run_collectstatic()
    class BuildStaticTestCase(StaticFilesTestCase):  
    86103
    87104    def tearDown(self):
    88105        settings.STATIC_ROOT = self.old_root
    89         super(BuildStaticTestCase, self).tearDown()
     106        super(BaseCollectionTestCase, self).tearDown()
    90107
    91108    def run_collectstatic(self, **kwargs):
    92109        call_command('collectstatic', interactive=False, verbosity='0',
    class BuildStaticTestCase(StaticFilesTestCase):  
    99116            return f.read()
    100117
    101118
     119class CollectionTestCase(BaseCollectionTestCase, StaticFilesTestCase):
     120    pass
     121
     122
    102123class TestDefaults(object):
    103124    """
    104125    A few standard test cases.
    class TestDefaults(object):  
    142163        self.assertFileContains(u'test/camelCase.txt', u'camelCase')
    143164
    144165
    145 class TestFindStatic(BuildStaticTestCase, TestDefaults):
     166class TestFindStatic(CollectionTestCase, TestDefaults):
    146167    """
    147168    Test ``findstatic`` management command.
    148169    """
    class TestFindStatic(BuildStaticTestCase, TestDefaults):  
    171192            lines = [l.strip() for l in sys.stdout.readlines()]
    172193        finally:
    173194            sys.stdout = _stdout
    174         self.assertEqual(len(lines), 3) # three because there is also the "Found <file> here" line
     195        self.assertEqual(len(lines), 3)  # three because there is also the "Found <file> here" line
    175196        self.assertTrue('project' in lines[1])
    176197        self.assertTrue('apps' in lines[2])
    177198
    178199
    179 class TestBuildStatic(BuildStaticTestCase, TestDefaults):
     200class TestCollection(CollectionTestCase, TestDefaults):
    180201    """
    181202    Test ``collectstatic`` management command.
    182203    """
    class TestBuildStatic(BuildStaticTestCase, TestDefaults):  
    195216        self.assertFileNotFound('test/CVS')
    196217
    197218
    198 class TestBuildStaticClear(BuildStaticTestCase):
     219class TestCollectionClear(CollectionTestCase):
    199220    """
    200221    Test the ``--clear`` option of the ``collectstatic`` managemenet command.
    201222    """
    class TestBuildStaticClear(BuildStaticTestCase):  
    203224        clear_filepath = os.path.join(settings.STATIC_ROOT, 'cleared.txt')
    204225        with open(clear_filepath, 'w') as f:
    205226            f.write('should be cleared')
    206         super(TestBuildStaticClear, self).run_collectstatic(clear=True)
     227        super(TestCollectionClear, self).run_collectstatic(clear=True)
    207228
    208229    def test_cleared_not_found(self):
    209230        self.assertFileNotFound('cleared.txt')
    210231
    211232
    212 class TestBuildStaticExcludeNoDefaultIgnore(BuildStaticTestCase, TestDefaults):
     233class TestCollectionExcludeNoDefaultIgnore(CollectionTestCase, TestDefaults):
    213234    """
    214235    Test ``--exclude-dirs`` and ``--no-default-ignore`` options of the
    215236    ``collectstatic`` management command.
    216237    """
    217238    def run_collectstatic(self):
    218         super(TestBuildStaticExcludeNoDefaultIgnore, self).run_collectstatic(
     239        super(TestCollectionExcludeNoDefaultIgnore, self).run_collectstatic(
    219240            use_default_ignore_patterns=False)
    220241
    221242    def test_no_common_ignore_patterns(self):
    class TestNoFilesCreated(object):  
    238259        self.assertEqual(os.listdir(settings.STATIC_ROOT), [])
    239260
    240261
    241 class TestBuildStaticDryRun(BuildStaticTestCase, TestNoFilesCreated):
     262class TestCollectionDryRun(CollectionTestCase, TestNoFilesCreated):
    242263    """
    243264    Test ``--dry-run`` option for ``collectstatic`` management command.
    244265    """
    245266    def run_collectstatic(self):
    246         super(TestBuildStaticDryRun, self).run_collectstatic(dry_run=True)
     267        super(TestCollectionDryRun, self).run_collectstatic(dry_run=True)
    247268
    248269
    249 class TestBuildStaticNonLocalStorage(BuildStaticTestCase, TestNoFilesCreated):
     270class TestCollectionNonLocalStorage(CollectionTestCase, TestNoFilesCreated):
    250271    """
    251272    Tests for #15035
    252273    """
    253274    pass
    254275
    255 TestBuildStaticNonLocalStorage = override_settings(
     276TestCollectionNonLocalStorage = override_settings(
    256277    STATICFILES_STORAGE='regressiontests.staticfiles_tests.storage.DummyStorage',
    257 )(TestBuildStaticNonLocalStorage)
     278)(TestCollectionNonLocalStorage)
     279
     280
     281class TestCollectionCachedStorage(BaseCollectionTestCase,
     282        BaseStaticFilesTestCase, TestCase):
     283    """
     284    Tests for the Cache busting storage
     285    """
     286    def cached_file_path(self, relpath):
     287        template = "{%% load static from staticfiles %%}{%% static '%s' %%}"
     288        fullpath = self.render_template(template % relpath)
     289        return fullpath.replace(settings.STATIC_URL, '')
     290
     291    def test_template_tag_return(self):
     292        """
     293        Test the CachedStaticFilesStorage backend.
     294        """
     295        self.assertTemplateRaises(ValueError, """
     296            {% load static from staticfiles %}{% static "does/not/exist.png" %}
     297            """, "/static/does/not/exist.png")
     298        self.assertTemplateRenders("""
     299            {% load static from staticfiles %}{% static "test/file.txt" %}
     300            """, "/static/test/file.dad0999e4f8f.txt")
     301        self.assertTemplateRenders("""
     302            {% load static from staticfiles %}{% static "cached/styles.css" %}
     303            """, "/static/cached/styles.5653c259030b.css")
     304
     305    def test_template_tag_simple_content(self):
     306        relpath = self.cached_file_path("cached/styles.css")
     307        self.assertEqual(relpath, "cached/styles.5653c259030b.css")
     308        with storage.staticfiles_storage.open(relpath) as relfile:
     309            content = relfile.read()
     310            self.assertFalse("cached/other.css" in content, content)
     311            self.assertTrue("/static/cached/other.d41d8cd98f00.css" in content)
     312
     313    def test_template_tag_absolute(self):
     314        relpath = self.cached_file_path("cached/absolute.css")
     315        self.assertEqual(relpath, "cached/absolute.cc80cb5e2eb1.css")
     316        with storage.staticfiles_storage.open(relpath) as relfile:
     317            content = relfile.read()
     318            self.assertFalse("/static/cached/styles.css" in content)
     319            self.assertTrue("/static/cached/styles.5653c259030b.css" in content)
     320
     321    def test_template_tag_denorm(self):
     322        relpath = self.cached_file_path("cached/denorm.css")
     323        self.assertEqual(relpath, "cached/denorm.363de96e9b4b.css")
     324        with storage.staticfiles_storage.open(relpath) as relfile:
     325            content = relfile.read()
     326            self.assertFalse("..//cached///styles.css" in content)
     327            self.assertTrue("/static/cached/styles.5653c259030b.css" in content)
     328
     329    def test_template_tag_relative(self):
     330        relpath = self.cached_file_path("cached/relative.css")
     331        self.assertEqual(relpath, "cached/relative.298ff891a8d4.css")
     332        with storage.staticfiles_storage.open(relpath) as relfile:
     333            content = relfile.read()
     334            self.assertFalse("../cached/styles.css" in content)
     335            self.assertFalse('@import "styles.css"' in content)
     336            self.assertTrue("/static/cached/styles.5653c259030b.css" in content)
     337
     338    def test_template_tag_url(self):
     339        relpath = self.cached_file_path("cached/url.css")
     340        self.assertEqual(relpath, "cached/url.615e21601e4b.css")
     341        with storage.staticfiles_storage.open(relpath) as relfile:
     342            self.assertTrue("https://" in relfile.read())
     343
     344# we set DEBUG to False here since the template tag wouldn't work otherwise
     345TestCollectionCachedStorage = override_settings(**dict(TEST_SETTINGS,
     346    STATICFILES_STORAGE='django.contrib.staticfiles.storage.CachedStaticFilesStorage',
     347    DEBUG=False,
     348))(TestCollectionCachedStorage)
    258349
    259350
    260351if sys.platform != 'win32':
    261     class TestBuildStaticLinks(BuildStaticTestCase, TestDefaults):
     352
     353    class TestCollectionLinks(CollectionTestCase, TestDefaults):
    262354        """
    263355        Test ``--link`` option for ``collectstatic`` management command.
    264356
    if sys.platform != 'win32':  
    267359        ``--link`` does not change the file-selection semantics.
    268360        """
    269361        def run_collectstatic(self):
    270             super(TestBuildStaticLinks, self).run_collectstatic(link=True)
     362            super(TestCollectionLinks, self).run_collectstatic(link=True)
    271363
    272364        def test_links_created(self):
    273365            """
    class TestServeStaticWithDefaultURL(TestServeStatic, TestDefaults):  
    312404    """
    313405    pass
    314406
     407
    315408class TestServeStaticWithURLHelper(TestServeStatic, TestDefaults):
    316409    """
    317410    Test static asset serving view with staticfiles_urlpatterns helper.
    class TestMiscFinder(TestCase):  
    399492            finders.FileSystemFinder))
    400493
    401494    def test_get_finder_bad_classname(self):
    402         self.assertRaises(ImproperlyConfigured,
    403             finders.get_finder, 'django.contrib.staticfiles.finders.FooBarFinder')
     495        self.assertRaises(ImproperlyConfigured, finders.get_finder,
     496                          'django.contrib.staticfiles.finders.FooBarFinder')
    404497
    405498    def test_get_finder_bad_module(self):
    406499        self.assertRaises(ImproperlyConfigured,
    407500            finders.get_finder, 'foo.bar.FooBarFinder')
    408501
    409 
    410 class TestStaticfilesDirsType(TestCase):
    411     """
    412     We can't determine if STATICFILES_DIRS is set correctly just by looking at
    413     the type, but we can determine if it's definitely wrong.
    414     """
    415502    def test_non_tuple_raises_exception(self):
    416         self.assertRaises(ImproperlyConfigured, finders.FileSystemFinder)
     503        """
     504        We can't determine if STATICFILES_DIRS is set correctly just by
     505        looking at the type, but we can determine if it's definitely wrong.
     506        """
     507        with self.settings(STATICFILES_DIRS='a string'):
     508            self.assertRaises(ImproperlyConfigured, finders.FileSystemFinder)
     509
     510
     511class TestTemplateTag(StaticFilesTestCase):
    417512
    418 TestStaticfilesDirsType = override_settings(
    419     STATICFILES_DIRS = 'a string',
    420 )(TestStaticfilesDirsType)
     513    def test_template_tag(self):
     514        self.assertTemplateRenders("""
     515            {% load static from staticfiles %}{% static "does/not/exist.png" %}
     516            """, "/static/does/not/exist.png")
     517        self.assertTemplateRenders("""
     518            {% load static from staticfiles %}{% static "testfile.txt" %}
     519            """, "/static/testfile.txt")
Back to Top