Ticket #15252: 15252.3.diff

File 15252.3.diff, 40.3 KB (added by jezdez, 4 years ago)

Latest state of the Github repo at https://github.com/jezdez/django/compare/feature/staticfiles-templatetag

  • 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..d7dc1a5 100644
    a b from django import forms 
    77from django.core.urlresolvers import reverse
    88from django.forms.widgets import RadioFieldRenderer
    99from django.forms.util import flatatt
    10 from django.templatetags.static import static
    1110from django.utils.html import escape
    1211from django.utils.text import Truncator
    1312from django.utils.translation import ugettext as _
    1413from django.utils.safestring import mark_safe
    1514from django.utils.encoding import force_unicode
     15from django.contrib.admin.templatetags.admin_static import static
     16
    1617
    1718class FilteredSelectMultiple(forms.SelectMultiple):
    1819    """
    class FilteredSelectMultiple(forms.SelectMultiple): 
    3132        super(FilteredSelectMultiple, self).__init__(attrs, choices)
    3233
    3334    def render(self, name, value, attrs=None, choices=()):
    34         if attrs is None: attrs = {}
     35        if attrs is None:
     36            attrs = {}
    3537        attrs['class'] = 'selectfilter'
    36         if self.is_stacked: attrs['class'] += 'stacked'
     38        if self.is_stacked:
     39            attrs['class'] += 'stacked'
    3740        output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)]
    3841        output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
    3942        # TODO: "id_" is hard-coded here. This should instead use the correct
  • 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..ead95ac 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."
     43    storage = storage.configured_storage
    4044
    4145    def __init__(self, *args, **kwargs):
    4246        super(NoArgsCommand, self).__init__(*args, **kwargs)
    4347        self.copied_files = []
    4448        self.symlinked_files = []
    4549        self.unmodified_files = []
    46         self.storage = get_storage_class(settings.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
    110113        for finder in finders.get_finders():
    Type 'yes' to continue, or 'no' to cancel: """ 
    116119                    prefixed_path = path
    117120                handler(path, prefixed_path, storage)
    118121
    119         actual_count = len(self.copied_files) + len(self.symlinked_files)
     122        modified_files = self.copied_files + self.symlinked_files
     123        actual_count = len(modified_files)
     124
     125        # Here we check if the storage backend has a post_process method
     126        # and pass it the list of modified files, if possible.
     127        if hasattr(self.storage, 'post_process'):
     128            self.storage.post_process(modified_files)
     129
    120130        unmodified_count = len(self.unmodified_files)
    121131        if self.verbosity >= 1:
    122132            self.stdout.write(smart_str(u"\n%s static file%s %s %s%s.\n"
    Type 'yes' to continue, or 'no' to cancel: """ 
    146156        for f in files:
    147157            fpath = os.path.join(path, f)
    148158            if self.dry_run:
    149                 self.log(u"Pretending to delete '%s'" % smart_unicode(fpath), level=1)
     159                self.log(u"Pretending to delete '%s'" %
     160                         smart_unicode(fpath), level=1)
    150161            else:
    151162                self.log(u"Deleting '%s'" % smart_unicode(fpath), level=1)
    152163                self.storage.delete(fpath)
    Type 'yes' to continue, or 'no' to cancel: """ 
    159170        if self.storage.exists(prefixed_path):
    160171            try:
    161172                # When was the target file modified last time?
    162                 target_last_modified = self.storage.modified_time(prefixed_path)
     173                target_last_modified = \
     174                    self.storage.modified_time(prefixed_path)
    163175            except (OSError, NotImplementedError):
    164176                # The storage doesn't support ``modified_time`` or failed
    165177                pass
    Type 'yes' to continue, or 'no' to cancel: """ 
    177189                        full_path = None
    178190                    # Skip the file if the source file is younger
    179191                    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))):
     192                        if not ((self.symlink and full_path
     193                                 and not os.path.islink(full_path)) or
     194                                (not self.symlink and full_path
     195                                 and os.path.islink(full_path))):
    182196                            if prefixed_path not in self.unmodified_files:
    183197                                self.unmodified_files.append(prefixed_path)
    184198                            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..fdb8b3d 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.storage import FileSystemStorage, get_storage_class
     11from django.utils.encoding import force_unicode
     12from django.utils.functional import LazyObject
    513from django.utils.importlib import import_module
    614
    715from django.contrib.staticfiles import utils
    class StaticFilesStorage(FileSystemStorage): 
    2735            raise ImproperlyConfigured("You're using the staticfiles app "
    2836                "without having set the STATIC_URL setting.")
    2937        utils.check_settings()
    30         super(StaticFilesStorage, self).__init__(location, base_url, *args, **kwargs)
     38        super(StaticFilesStorage, self).__init__(location, base_url,
     39                                                 *args, **kwargs)
     40
     41
     42class CachedFilesMixin(object):
     43    cached_patterns = [
     44        r"""(url\(['"]{0,1}\s*(.*?)["']{0,1}\))""",
     45        r"""(@import\s*["']\s*(.*?)["'])""",
     46    ]
     47
     48    def __init__(self, *args, **kwargs):
     49        super(CachedFilesMixin, self).__init__(*args, **kwargs)
     50        self.saved_files = []
     51        try:
     52            self.cache = get_cache('staticfiles')
     53        except InvalidCacheBackendError:
     54            # Use the default backend
     55            self.cache = default_cache
     56        self._cached_patterns = []
     57        for pattern in self.cached_patterns:
     58            self._cached_patterns.append(re.compile(pattern))
     59
     60    def hashed_name(self, name, content=None):
     61        if content is None:
     62            if not self.exists(name):
     63                raise ValueError("The file '%s' could not be found using %r." %
     64                                 (name, self))
     65            try:
     66                content = self.open(name)
     67            except IOError, e:
     68                # Handle directory paths
     69                return name
     70        path, filename = os.path.split(name)
     71        root, ext = os.path.splitext(filename)
     72        # Get the MD5 hash of the file
     73        md5 = hashlib.md5()
     74        for chunk in content.chunks():
     75            md5.update(chunk)
     76        md5sum = md5.hexdigest()[:12]
     77        return os.path.join(path, u"%s.%s%s" % (root, md5sum, ext))
     78
     79    def cache_key(self, name):
     80        return u'staticfiles:cache:%s' % name
     81
     82    def url(self, name, force=False):
     83        """
     84        Returns the real URL in DEBUG mode.
     85        """
     86        if settings.DEBUG and not force:
     87            return super(CachedFilesMixin, self).url(name)
     88        cache_key = self.cache_key(name)
     89        hashed_name = self.cache.get(cache_key)
     90        if hashed_name is None:
     91            hashed_name = self.hashed_name(name)
     92        return super(CachedFilesMixin, self).url(hashed_name)
     93
     94    def save(self, name, content):
     95        original_name = super(CachedFilesMixin, self).save(name, content)
     96
     97        # Return the name if the file is already there
     98        hashed_name = self.hashed_name(original_name, content)
     99        if os.path.exists(hashed_name):
     100            return hashed_name
     101
     102        # or save the file with the hashed name
     103        saved_name = self._save(hashed_name, content)
     104
     105        # Use filenames with forward slashes, even on Windows
     106        hashed_name = force_unicode(saved_name.replace('\\', '/'))
     107        self.cache.set(self.cache_key(name), hashed_name)
     108        self.saved_files.append((name, hashed_name))
     109        return hashed_name
     110
     111    def url_converter(self, name):
     112        """
     113        Returns the custom URL converter for the given file name.
     114        """
     115        def converter(matchobj):
     116            """
     117            Converts the matched URL depending on the parent level (`..`)
     118            and returns the normalized and hashed URL using the url method
     119            of the storage.
     120            """
     121            matched, url = matchobj.groups()
     122            # Completely ignore http(s) prefixed URLs
     123            if url.startswith(('http', 'https')):
     124                return matched
     125            name_parts = name.split('/')
     126            # Using posix normpath here to remove duplicates
     127            result = url_parts = posixpath.normpath(url).split('/')
     128            level = url.count('..')
     129            if level:
     130                result = name_parts[:-level-1] + url_parts[level:]
     131            elif name_parts[:-1]:
     132                result = name_parts[:-1] + url_parts[-1:]
     133            joined_result = '/'.join(result)
     134            hashed_url = self.url(joined_result, force=True)
     135            # Return the hashed and normalized version to the file
     136            return 'url("%s")' % hashed_url
     137        return converter
     138
     139    def path_level(self, (name, hashed_name)):
     140        return len(name.split('/'))
     141
     142    def post_process(self, paths):
     143        """
     144        Post process method called by the collectstatic management command.
     145        """
     146        self.cache.delete_many([self.cache_key(path) for path in paths])
     147        for name, hashed_name in sorted(
     148                self.saved_files, key=self.path_level, reverse=True):
     149            with self.open(name) as original_file:
     150                content = original_file.read()
     151                for pattern in self._cached_patterns:
     152                    content = pattern.sub(self.url_converter(name), content)
     153            with open(self.path(hashed_name), 'w') as hashed_file:
     154                hashed_file.write(content)
     155
     156
     157class CachedStaticFilesStorage(CachedFilesMixin, StaticFilesStorage):
     158    """
     159    A static file system storage backend which also saves
     160    hashed copies of the files it saves.
     161    """
     162    pass
    31163
    32164
    33165class AppStaticStorage(FileSystemStorage):
    class AppStaticStorage(FileSystemStorage): 
    47179        mod_path = os.path.dirname(mod.__file__)
    48180        location = os.path.join(mod_path, self.source_dir)
    49181        super(AppStaticStorage, self).__init__(location, *args, **kwargs)
     182
     183
     184class ConfiguredStorage(LazyObject):
     185    def _setup(self):
     186        self._wrapped = get_storage_class(settings.STATICFILES_STORAGE)()
     187
     188configured_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..80cf53e
    - +  
     1from django import template
     2from django.contrib.staticfiles.storage import configured_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 configured_storage.url(path)
  • 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..abf8c58 100644
    a b Example usage:: 
    237237
    238238    django-admin.py runserver --insecure
    239239
    240 .. currentmodule:: None
     240Storages
     241========
     242
     243StaticFilesStorage
     244------------------
     245
     246.. class:: storage.StaticFilesStorage
     247
     248A subclass of the :class:`~django.core.files.storage.FileSystemStorage`
     249storage backend that uses the :setting:`STATIC_ROOT` setting as the base
     250file system location and the :setting:`STATIC_URL` setting respectively
     251as the base URL.
     252
     253CachedStaticFilesStorage
     254------------------------
     255
     256.. class:: storage.CachedStaticFilesStorage
     257
     258A subclass of the :class:`~django.contrib.staticfiles.storage.StaticFilesStorage`
     259storage backend which caches the files it saves by appending the MD5 hash of
     260the file's content to the filename. For example, the file ``css/styles.css``
     261would also be saved as ``css/styles.55e7cbb9ba48.css``.
     262
     263The purpose of this storage is to keep serving the old files in case some
     264pages still refer to those files, e.g. because they are cached by you or a
     2653rd party proxy server. Additionally, it's very helpful if you want to apply
     266`far future Expires headers`_ to the deployed files to speed up the load time
     267for subsequent page visits.
     268
     269The storage backend automatically replaces the paths found in the saved files
     270matching other saved files with the path of the cached copy. The regular
     271expressions used to find those paths
     272(``django.contrib.staticfiles.storage.CachedStaticFilesStorage.cached_patterns``)
     273by default cover the `@import`_ rule and `url()`_ statement of `Cascading
     274Style Sheets`_. For example, the ``'css/styles.css'`` file with the content
     275
     276.. code-block:: css+django
     277
     278    @import url("../admin/css/base.css");
     279
     280would be replaced by calling the :meth:`~django.core.files.storage.Storage.url`
     281method of the ``CachedStaticFilesStorage`` storage backend, ultimatively
     282saving a ``'css/styles.55e7cbb9ba48.css'`` file with the following content:
     283
     284.. code-block:: css+django
     285
     286    @import url("/static/admin/css/base.27e20196a850.css");
     287
     288To enable the ``CachedStaticFilesStorage`` you have to make sure the
     289following requirements are met:
     290
     291* the :setting:`STATICFILES_STORAGE` setting is set to
     292  ``'django.contrib.staticfiles.storage.CachedStaticFilesStorage'``
     293* the :setting:`DEBUG` setting is set to ``False``
     294* you use the ``staticfiles`` :ttag:`static<staticfiles-static>` template tag
     295  to refer to your static files in your templates
     296* you've collected all your static files by using the :djadmin:`collectstatic`
     297  management command
     298
     299Since creating the MD5 hash can be a performance burden to your website during
     300runtime, ``staticfiles`` will automatically try to cache the hashed name for
     301each file path using Django's :doc:`caching framework</topics/cache>`. If you
     302want to override certain options of the cache backend the storage uses,
     303simply specify a custom entry in the :setting:`CACHES` setting named
     304``'staticfiles'``. It falls back to using the default cache backend.
     305
     306.. _`far future Expires headers`: http://developer.yahoo.com/performance/rules.html#expires
     307.. _`@import`: http://www.w3.org/TR/CSS2/cascade.html#at-import
     308.. _`url()`: http://www.w3.org/TR/CSS2/syndata.html#uri
     309.. _`Cascading Style Sheets`: http://www.w3.org/Style/CSS/
     310
     311.. currentmodule:: django.contrib.staticfiles.templatetags.staticfiles
     312
     313Template tags
     314=============
     315
     316static
     317------
     318
     319.. templatetag:: staticfiles-static
     320
     321.. versionadded:: 1.4
     322
     323Uses the configued :setting:`STATICFILES_STORAGE` storage to create the
     324full URL for the given relative path, e.g.:
     325
     326.. code-block:: html+django
     327
     328    {% load static from staticfiles %}
     329    <img src="{% static "css/base.css" %}" />
     330
     331The previous example is equal to calling the ``url`` method of an instance of
     332:setting:`STATICFILES_STORAGE` with ``"css/base.css"``. This is especially
     333useful when using a non-local storage backend to deploy files as documented
     334in :ref:`staticfiles-from-cdn`.
    241335
    242336Other Helpers
    243337=============
    files: 
    251345      with :class:`~django.template.RequestContext` contexts.
    252346
    253347    - The builtin template tag :ttag:`static` which takes a path and
    254       joins it with the the static prefix :setting:`STATIC_URL`.
     348      urljoins it with the static prefix :setting:`STATIC_URL`.
    255349
    256350    - The builtin template tag :ttag:`get_static_prefix` which populates a
    257351      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..ed276bb 100644
    a b import sys 
    88import tempfile
    99from StringIO import StringIO
    1010
     11from django.template import loader, Context
    1112from django.conf import settings
    12 from django.core.exceptions import ImproperlyConfigured
     13from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
    1314from django.core.files.storage import default_storage
    1415from django.core.management import call_command
    1516from django.test import TestCase
    class StaticFilesTestCase(TestCase): 
    4849    def assertFileNotFound(self, filepath):
    4950        self.assertRaises(IOError, self._get_file, filepath)
    5051
     52    def render_template(self, template, **kwargs):
     53        if isinstance(template, basestring):
     54            template = loader.get_template_from_string(template)
     55        return template.render(Context(kwargs)).strip()
     56
     57    def assertTemplateRenders(self, template, result, **kwargs):
     58        self.assertEqual(self.render_template(template, **kwargs), result)
     59
     60    def assertTemplateRaises(self, exc, template, result, **kwargs):
     61        self.assertRaises(exc, self.assertTemplateRenders, template, result, **kwargs)
     62
     63
    5164StaticFilesTestCase = override_settings(
    5265    DEBUG = True,
    5366    MEDIA_URL = '/media/',
    TestBuildStaticNonLocalStorage = override_settings( 
    257270)(TestBuildStaticNonLocalStorage)
    258271
    259272
     273class TestBuildStaticCachedStorage(BuildStaticTestCase):
     274    """
     275    Tests for the Cache busting storage
     276    """
     277    @classmethod
     278    def setUpClass(cls):
     279        storage.configured_storage._wrapped = empty
     280
     281    def tearDown(self):
     282        storage.configured_storage._wrapped = empty
     283
     284    def cached_file_path(self, relpath):
     285        template = "{%% load static from staticfiles %%}{%% static '%s' %%}"
     286        fullpath = self.render_template(template % relpath)
     287        return fullpath.replace(settings.STATIC_URL, '')
     288
     289    def test_template_tag_return(self):
     290        """
     291        Test the CachedStaticFilesStorage backend.
     292        """
     293        self.assertTemplateRaises(SuspiciousOperation, """
     294            {% load static from staticfiles %}{% static "does/not/exist.png" %}
     295            """, "/static/does/not/exist.png")
     296        self.assertTemplateRenders("""
     297            {% load static from staticfiles %}{% static "test/file.txt" %}
     298            """, "/static/test/file.dad0999e4f8f.txt")
     299        self.assertTemplateRenders("""
     300            {% load static from staticfiles %}{% static "cached/styles.css" %}
     301            """, "/static/cached/styles.5653c259030b.css")
     302
     303    def test_template_tag_simple_content(self):
     304        relpath = self.cached_file_path("cached/styles.css")
     305        self.assertEqual(relpath, "cached/styles.5653c259030b.css")
     306        with storage.configured_storage.open(relpath) as relfile:
     307            content = relfile.read()
     308            self.assertFalse("cached/other.css" in content)
     309            self.assertTrue("/static/cached/other.d41d8cd98f00.css" in content)
     310
     311    def test_template_tag_absolute(self):
     312        relpath = self.cached_file_path("cached/absolute.css")
     313        self.assertEqual(relpath, "cached/absolute.cc80cb5e2eb1.css")
     314        with storage.configured_storage.open(relpath) as relfile:
     315            content = relfile.read()
     316            self.assertFalse("/static/cached/styles.css" in content)
     317            self.assertTrue("/static/cached/styles.5653c259030b.css" in content)
     318
     319    def test_template_tag_denorm(self):
     320        relpath = self.cached_file_path("cached/denorm.css")
     321        self.assertEqual(relpath, "cached/denorm.363de96e9b4b.css")
     322        with storage.configured_storage.open(relpath) as relfile:
     323            content = relfile.read()
     324            self.assertFalse("..//cached///styles.css" in content)
     325            self.assertTrue("/static/cached/styles.5653c259030b.css" in content)
     326
     327    def test_template_tag_relative(self):
     328        relpath = self.cached_file_path("cached/relative.css")
     329        self.assertEqual(relpath, "cached/relative.298ff891a8d4.css")
     330        with storage.configured_storage.open(relpath) as relfile:
     331            content = relfile.read()
     332            self.assertFalse("../cached/styles.css" in content)
     333            self.assertFalse('@import "styles.css"' in content)
     334            self.assertTrue("/static/cached/styles.5653c259030b.css" in content)
     335
     336    def test_template_tag_url(self):
     337        relpath = self.cached_file_path("cached/url.css")
     338        self.assertEqual(relpath, "cached/url.615e21601e4b.css")
     339        with storage.configured_storage.open(relpath) as relfile:
     340            self.assertTrue("https://" in relfile.read())
     341
     342
     343TestBuildStaticCachedStorage = override_settings(
     344    STATICFILES_STORAGE='django.contrib.staticfiles.storage.CachedStaticFilesStorage',
     345)(TestBuildStaticCachedStorage)
     346
     347
    260348if sys.platform != 'win32':
    261349    class TestBuildStaticLinks(BuildStaticTestCase, TestDefaults):
    262350        """
    class TestStaticfilesDirsType(TestCase): 
    418506TestStaticfilesDirsType = override_settings(
    419507    STATICFILES_DIRS = 'a string',
    420508)(TestStaticfilesDirsType)
     509
     510
     511class TestTemplateTag(StaticFilesTestCase):
     512
     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