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