Fixed #12323 and #11582 -- Extended the ability to handle static files. Thanks to all for helping with the original app, the patch, documentation and general support.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@14293 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
a014ee0288
commit
cfc19f84de
|
@ -194,7 +194,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
|||
'django.contrib.auth.context_processors.auth',
|
||||
'django.core.context_processors.debug',
|
||||
'django.core.context_processors.i18n',
|
||||
'django.core.context_processors.media',
|
||||
'django.contrib.staticfiles.context_processors.staticfiles',
|
||||
# 'django.core.context_processors.request',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
)
|
||||
|
@ -202,11 +202,6 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
|||
# Output to use in template system for invalid (e.g. misspelled) variables.
|
||||
TEMPLATE_STRING_IF_INVALID = ''
|
||||
|
||||
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://foo.com/media/", "/media/".
|
||||
ADMIN_MEDIA_PREFIX = '/media/'
|
||||
|
||||
# Default e-mail address to use for various automated correspondence from
|
||||
# the site managers.
|
||||
DEFAULT_FROM_EMAIL = 'webmaster@localhost'
|
||||
|
@ -551,3 +546,34 @@ TEST_DATABASE_COLLATION = None
|
|||
|
||||
# The list of directories to search for fixtures
|
||||
FIXTURE_DIRS = ()
|
||||
|
||||
###############
|
||||
# STATICFILES #
|
||||
###############
|
||||
|
||||
# Absolute path to the directory that holds media.
|
||||
# Example: "/home/media/media.lawrence.com/static/"
|
||||
STATICFILES_ROOT = ''
|
||||
|
||||
# URL that handles the static files served from STATICFILES_ROOT.
|
||||
# Example: "http://media.lawrence.com/static/"
|
||||
STATICFILES_URL = '/static/'
|
||||
|
||||
# A list of locations of additional static files
|
||||
STATICFILES_DIRS = ()
|
||||
|
||||
# The default file storage backend used during the build process
|
||||
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
|
||||
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
|
||||
)
|
||||
|
||||
# URL prefix for admin media -- CSS, JavaScript and images.
|
||||
# Make sure to use a trailing slash.
|
||||
# Examples: "http://foo.com/static/admin/", "/static/admin/".
|
||||
ADMIN_MEDIA_PREFIX = '/static/admin/'
|
||||
|
|
|
@ -44,7 +44,7 @@ USE_I18N = True
|
|||
USE_L10N = True
|
||||
|
||||
# Absolute path to the directory that holds media.
|
||||
# Example: "/home/media/media.lawrence.com/"
|
||||
# Example: "/home/media/media.lawrence.com/media/"
|
||||
MEDIA_ROOT = ''
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
|
@ -52,10 +52,29 @@ MEDIA_ROOT = ''
|
|||
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
||||
MEDIA_URL = ''
|
||||
|
||||
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://foo.com/media/", "/media/".
|
||||
ADMIN_MEDIA_PREFIX = '/media/'
|
||||
# Absolute path to the directory that holds media.
|
||||
# Example: "/home/media/media.lawrence.com/static/"
|
||||
STATICFILES_ROOT = ''
|
||||
|
||||
# URL that handles the static files served from STATICFILES_ROOT.
|
||||
# Example: "http://static.lawrence.com/", "http://example.com/static/"
|
||||
STATICFILES_URL = '/static/'
|
||||
|
||||
# URL prefix for admin media -- CSS, JavaScript and images.
|
||||
# Make sure to use a trailing slash.
|
||||
# Examples: "http://foo.com/static/admin/", "/static/admin/".
|
||||
ADMIN_MEDIA_PREFIX = '/static/admin/'
|
||||
|
||||
# A list of locations of additional static files
|
||||
STATICFILES_DIRS = ()
|
||||
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
|
||||
)
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = ''
|
||||
|
@ -89,6 +108,7 @@ INSTALLED_APPS = (
|
|||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
# Uncomment the next line to enable the admin:
|
||||
# 'django.contrib.admin',
|
||||
# Uncomment the next line to enable admin documentation:
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
from django.conf import settings
|
||||
|
||||
def staticfiles(request):
|
||||
return {
|
||||
'STATICFILES_URL': settings.STATICFILES_URL,
|
||||
'MEDIA_URL': settings.MEDIA_URL,
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
import os
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.files.storage import default_storage, Storage, FileSystemStorage
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.functional import memoize, LazyObject
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
from django.contrib.staticfiles import utils
|
||||
from django.contrib.staticfiles.storage import AppStaticStorage
|
||||
|
||||
_finders = {}
|
||||
|
||||
|
||||
class BaseFinder(object):
|
||||
"""
|
||||
A base file finder to be used for custom staticfiles finder classes.
|
||||
|
||||
"""
|
||||
def find(self, path, all=False):
|
||||
"""
|
||||
Given a relative file path this ought to find an
|
||||
absolute file path.
|
||||
|
||||
If the ``all`` parameter is ``False`` (default) only
|
||||
the first found file path will be returned; if set
|
||||
to ``True`` a list of all found files paths is returned.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def list(self, ignore_patterns=[]):
|
||||
"""
|
||||
Given an optional list of paths to ignore, this should return
|
||||
a three item iterable with path, prefix and a storage instance.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class FileSystemFinder(BaseFinder):
|
||||
"""
|
||||
A static files finder that uses the ``STATICFILES_DIRS`` setting
|
||||
to locate files.
|
||||
"""
|
||||
storages = SortedDict()
|
||||
locations = set()
|
||||
|
||||
def __init__(self, apps=None, *args, **kwargs):
|
||||
for root in settings.STATICFILES_DIRS:
|
||||
if isinstance(root, (list, tuple)):
|
||||
prefix, root = root
|
||||
else:
|
||||
prefix = ''
|
||||
self.locations.add((prefix, root))
|
||||
# Don't initialize multiple storages for the same location
|
||||
for prefix, root in self.locations:
|
||||
self.storages[root] = FileSystemStorage(location=root)
|
||||
super(FileSystemFinder, self).__init__(*args, **kwargs)
|
||||
|
||||
def find(self, path, all=False):
|
||||
"""
|
||||
Looks for files in the extra media locations
|
||||
as defined in ``STATICFILES_DIRS``.
|
||||
"""
|
||||
matches = []
|
||||
for prefix, root in self.locations:
|
||||
matched_path = self.find_location(root, path, prefix)
|
||||
if matched_path:
|
||||
if not all:
|
||||
return matched_path
|
||||
matches.append(matched_path)
|
||||
return matches
|
||||
|
||||
def find_location(self, root, path, prefix=None):
|
||||
"""
|
||||
Find a requested static file in a location, returning the found
|
||||
absolute path (or ``None`` if no match).
|
||||
"""
|
||||
if prefix:
|
||||
prefix = '%s/' % prefix
|
||||
if not path.startswith(prefix):
|
||||
return None
|
||||
path = path[len(prefix):]
|
||||
path = os.path.join(root, path)
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
|
||||
def list(self, ignore_patterns):
|
||||
"""
|
||||
List all files in all locations.
|
||||
"""
|
||||
for prefix, root in self.locations:
|
||||
storage = self.storages[root]
|
||||
for path in utils.get_files(storage, ignore_patterns):
|
||||
yield path, prefix, storage
|
||||
|
||||
|
||||
class AppDirectoriesFinder(BaseFinder):
|
||||
"""
|
||||
A static files finder that looks in the ``media`` directory of each app.
|
||||
"""
|
||||
storages = {}
|
||||
storage_class = AppStaticStorage
|
||||
|
||||
def __init__(self, apps=None, *args, **kwargs):
|
||||
if apps is not None:
|
||||
self.apps = apps
|
||||
else:
|
||||
self.apps = models.get_apps()
|
||||
for app in self.apps:
|
||||
self.storages[app] = self.storage_class(app)
|
||||
super(AppDirectoriesFinder, self).__init__(*args, **kwargs)
|
||||
|
||||
def list(self, ignore_patterns):
|
||||
"""
|
||||
List all files in all app storages.
|
||||
"""
|
||||
for storage in self.storages.itervalues():
|
||||
if storage.exists(''): # check if storage location exists
|
||||
prefix = storage.get_prefix()
|
||||
for path in utils.get_files(storage, ignore_patterns):
|
||||
yield path, prefix, storage
|
||||
|
||||
def find(self, path, all=False):
|
||||
"""
|
||||
Looks for files in the app directories.
|
||||
"""
|
||||
matches = []
|
||||
for app in self.apps:
|
||||
app_matches = self.find_in_app(app, path)
|
||||
if app_matches:
|
||||
if not all:
|
||||
return app_matches
|
||||
matches.append(app_matches)
|
||||
return matches
|
||||
|
||||
def find_in_app(self, app, path):
|
||||
"""
|
||||
Find a requested static file in an app's media locations.
|
||||
"""
|
||||
storage = self.storages[app]
|
||||
prefix = storage.get_prefix()
|
||||
if prefix:
|
||||
prefix = '%s/' % prefix
|
||||
if not path.startswith(prefix):
|
||||
return None
|
||||
path = path[len(prefix):]
|
||||
# only try to find a file if the source dir actually exists
|
||||
if storage.exists(path):
|
||||
matched_path = storage.path(path)
|
||||
if matched_path:
|
||||
return matched_path
|
||||
|
||||
|
||||
class BaseStorageFinder(BaseFinder):
|
||||
"""
|
||||
A base static files finder to be used to extended
|
||||
with an own storage class.
|
||||
"""
|
||||
storage = None
|
||||
|
||||
def __init__(self, storage=None, *args, **kwargs):
|
||||
if storage is not None:
|
||||
self.storage = storage
|
||||
if self.storage is None:
|
||||
raise ImproperlyConfigured("The staticfiles storage finder %r "
|
||||
"doesn't have a storage class "
|
||||
"assigned." % self.__class__)
|
||||
# Make sure we have an storage instance here.
|
||||
if not isinstance(self.storage, (Storage, LazyObject)):
|
||||
self.storage = self.storage()
|
||||
super(BaseStorageFinder, self).__init__(*args, **kwargs)
|
||||
|
||||
def find(self, path, all=False):
|
||||
"""
|
||||
Looks for files in the default file storage, if it's local.
|
||||
"""
|
||||
try:
|
||||
self.storage.path('')
|
||||
except NotImplementedError:
|
||||
pass
|
||||
else:
|
||||
if self.storage.exists(path):
|
||||
match = self.storage.path(path)
|
||||
if all:
|
||||
match = [match]
|
||||
return match
|
||||
return []
|
||||
|
||||
def list(self, ignore_patterns):
|
||||
"""
|
||||
List all files of the storage.
|
||||
"""
|
||||
for path in utils.get_files(self.storage, ignore_patterns):
|
||||
yield path, '', self.storage
|
||||
|
||||
class DefaultStorageFinder(BaseStorageFinder):
|
||||
"""
|
||||
A static files finder that uses the default storage backend.
|
||||
"""
|
||||
storage = default_storage
|
||||
|
||||
|
||||
def find(path, all=False):
|
||||
"""
|
||||
Find a requested static file, first looking in any defined extra media
|
||||
locations and next in any (non-excluded) installed apps.
|
||||
|
||||
If no matches are found and the static location is local, look for a match
|
||||
there too.
|
||||
|
||||
If ``all`` is ``False`` (default), return the first matching
|
||||
absolute path (or ``None`` if no match). Otherwise return a list of
|
||||
found absolute paths.
|
||||
|
||||
"""
|
||||
matches = []
|
||||
for finder in get_finders():
|
||||
result = finder.find(path, all=all)
|
||||
if not all and result:
|
||||
return result
|
||||
if not isinstance(result, (list, tuple)):
|
||||
result = [result]
|
||||
matches.extend(result)
|
||||
if matches:
|
||||
return matches
|
||||
# No match.
|
||||
return all and [] or None
|
||||
|
||||
def get_finders():
|
||||
for finder_path in settings.STATICFILES_FINDERS:
|
||||
yield get_finder(finder_path)
|
||||
|
||||
def _get_finder(import_path):
|
||||
"""
|
||||
Imports the message storage class described by import_path, where
|
||||
import_path is the full Python path to the class.
|
||||
"""
|
||||
module, attr = import_path.rsplit('.', 1)
|
||||
try:
|
||||
mod = import_module(module)
|
||||
except ImportError, e:
|
||||
raise ImproperlyConfigured('Error importing module %s: "%s"' %
|
||||
(module, e))
|
||||
try:
|
||||
Finder = getattr(mod, attr)
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured('Module "%s" does not define a "%s" '
|
||||
'class.' % (module, attr))
|
||||
if not issubclass(Finder, BaseFinder):
|
||||
raise ImproperlyConfigured('Finder "%s" is not a subclass of "%s"' %
|
||||
(Finder, BaseFinder))
|
||||
return Finder()
|
||||
get_finder = memoize(_get_finder, _finders, 1)
|
|
@ -0,0 +1,72 @@
|
|||
import os
|
||||
import urllib
|
||||
from urlparse import urlparse
|
||||
|
||||
from django.core.handlers.wsgi import WSGIHandler, STATUS_CODE_TEXT
|
||||
from django.http import Http404
|
||||
|
||||
from django.contrib.staticfiles.views import serve
|
||||
|
||||
class StaticFilesHandler(WSGIHandler):
|
||||
"""
|
||||
WSGI middleware that intercepts calls to the static files directory, as
|
||||
defined by the STATICFILES_URL setting, and serves those files.
|
||||
"""
|
||||
def __init__(self, application, media_dir=None):
|
||||
self.application = application
|
||||
if media_dir:
|
||||
self.media_dir = media_dir
|
||||
else:
|
||||
self.media_dir = self.get_media_dir()
|
||||
self.media_url = self.get_media_url()
|
||||
|
||||
def get_media_dir(self):
|
||||
from django.conf import settings
|
||||
return settings.STATICFILES_ROOT
|
||||
|
||||
def get_media_url(self):
|
||||
from django.conf import settings
|
||||
return settings.STATICFILES_URL
|
||||
|
||||
def file_path(self, url):
|
||||
"""
|
||||
Returns the relative path to the media file on disk for the given URL.
|
||||
|
||||
The passed URL is assumed to begin with ``media_url``. If the
|
||||
resultant file path is outside the media directory, then a ValueError
|
||||
is raised.
|
||||
"""
|
||||
# Remove ``media_url``.
|
||||
relative_url = url[len(self.media_url):]
|
||||
return urllib.url2pathname(relative_url)
|
||||
|
||||
def serve(self, request, path):
|
||||
from django.contrib.staticfiles import finders
|
||||
absolute_path = finders.find(path)
|
||||
if not absolute_path:
|
||||
raise Http404('%r could not be matched to a static file.' % path)
|
||||
absolute_path, filename = os.path.split(absolute_path)
|
||||
return serve(request, path=filename, document_root=absolute_path)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
media_url_bits = urlparse(self.media_url)
|
||||
# Ignore all requests if the host is provided as part of the media_url.
|
||||
# Also ignore requests that aren't under the media path.
|
||||
if (media_url_bits[1] or
|
||||
not environ['PATH_INFO'].startswith(media_url_bits[2])):
|
||||
return self.application(environ, start_response)
|
||||
request = self.application.request_class(environ)
|
||||
try:
|
||||
response = self.serve(request, self.file_path(environ['PATH_INFO']))
|
||||
except Http404:
|
||||
status = '404 NOT FOUND'
|
||||
start_response(status, {'Content-type': 'text/plain'}.items())
|
||||
return [str('Page not found: %s' % environ['PATH_INFO'])]
|
||||
status_text = STATUS_CODE_TEXT[response.status_code]
|
||||
status = '%s %s' % (response.status_code, status_text)
|
||||
response_headers = [(str(k), str(v)) for k, v in response.items()]
|
||||
for c in response.cookies.values():
|
||||
response_headers.append(('Set-Cookie', str(c.output(header=''))))
|
||||
start_response(status, response_headers)
|
||||
return response
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
import os
|
||||
import sys
|
||||
import shutil
|
||||
from optparse import make_option
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import get_storage_class
|
||||
from django.core.management.base import CommandError, NoArgsCommand
|
||||
|
||||
from django.contrib.staticfiles import finders
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
"""
|
||||
Command that allows to copy or symlink media files from different
|
||||
locations to the settings.STATICFILES_ROOT.
|
||||
"""
|
||||
option_list = NoArgsCommand.option_list + (
|
||||
make_option('--noinput', action='store_false', dest='interactive',
|
||||
default=True, help="Do NOT prompt the user for input of any "
|
||||
"kind."),
|
||||
make_option('-i', '--ignore', action='append', default=[],
|
||||
dest='ignore_patterns', metavar='PATTERN',
|
||||
help="Ignore files or directories matching this glob-style "
|
||||
"pattern. Use multiple times to ignore more."),
|
||||
make_option('-n', '--dry-run', action='store_true', dest='dry_run',
|
||||
default=False, help="Do everything except modify the filesystem."),
|
||||
make_option('-l', '--link', action='store_true', dest='link',
|
||||
default=False, help="Create a symbolic link to each file instead of copying."),
|
||||
make_option('--no-default-ignore', action='store_false',
|
||||
dest='use_default_ignore_patterns', default=True,
|
||||
help="Don't ignore the common private glob-style patterns 'CVS', "
|
||||
"'.*' and '*~'."),
|
||||
)
|
||||
help = "Collect static files from apps and other locations in a single location."
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
symlink = options['link']
|
||||
ignore_patterns = options['ignore_patterns']
|
||||
if options['use_default_ignore_patterns']:
|
||||
ignore_patterns += ['CVS', '.*', '*~']
|
||||
ignore_patterns = list(set(ignore_patterns))
|
||||
self.copied_files = []
|
||||
self.symlinked_files = []
|
||||
self.unmodified_files = []
|
||||
self.destination_storage = get_storage_class(settings.STATICFILES_STORAGE)()
|
||||
|
||||
try:
|
||||
self.destination_storage.path('')
|
||||
except NotImplementedError:
|
||||
self.destination_local = False
|
||||
else:
|
||||
self.destination_local = True
|
||||
|
||||
if symlink:
|
||||
if sys.platform == 'win32':
|
||||
raise CommandError("Symlinking is not supported by this "
|
||||
"platform (%s)." % sys.platform)
|
||||
if not self.destination_local:
|
||||
raise CommandError("Can't symlink to a remote destination.")
|
||||
|
||||
# Warn before doing anything more.
|
||||
if options.get('interactive'):
|
||||
confirm = raw_input("""
|
||||
You have requested to collate static files and collect them at the destination
|
||||
location as specified in your settings file.
|
||||
|
||||
This will overwrite existing files.
|
||||
Are you sure you want to do this?
|
||||
|
||||
Type 'yes' to continue, or 'no' to cancel: """)
|
||||
if confirm != 'yes':
|
||||
raise CommandError("Static files build cancelled.")
|
||||
|
||||
for finder in finders.get_finders():
|
||||
for source, prefix, storage in finder.list(ignore_patterns):
|
||||
self.copy_file(source, prefix, storage, **options)
|
||||
|
||||
verbosity = int(options.get('verbosity', 1))
|
||||
actual_count = len(self.copied_files) + len(self.symlinked_files)
|
||||
unmodified_count = len(self.unmodified_files)
|
||||
if verbosity >= 1:
|
||||
self.stdout.write("\n%s static file%s %s to '%s'%s.\n"
|
||||
% (actual_count, actual_count != 1 and 's' or '',
|
||||
symlink and 'symlinked' or 'copied',
|
||||
settings.STATICFILES_ROOT,
|
||||
unmodified_count and ' (%s unmodified)'
|
||||
% unmodified_count or ''))
|
||||
|
||||
def copy_file(self, source, prefix, source_storage, **options):
|
||||
"""
|
||||
Attempt to copy (or symlink) ``source`` to ``destination``,
|
||||
returning True if successful.
|
||||
"""
|
||||
source_path = source_storage.path(source)
|
||||
try:
|
||||
source_last_modified = source_storage.modified_time(source)
|
||||
except (OSError, NotImplementedError):
|
||||
source_last_modified = None
|
||||
if prefix:
|
||||
destination = '/'.join([prefix, source])
|
||||
else:
|
||||
destination = source
|
||||
symlink = options['link']
|
||||
dry_run = options['dry_run']
|
||||
verbosity = int(options.get('verbosity', 1))
|
||||
|
||||
if destination in self.copied_files:
|
||||
if verbosity >= 2:
|
||||
self.stdout.write("Skipping '%s' (already copied earlier)\n"
|
||||
% destination)
|
||||
return False
|
||||
|
||||
if destination in self.symlinked_files:
|
||||
if verbosity >= 2:
|
||||
self.stdout.write("Skipping '%s' (already linked earlier)\n"
|
||||
% destination)
|
||||
return False
|
||||
|
||||
if self.destination_storage.exists(destination):
|
||||
try:
|
||||
destination_last_modified = \
|
||||
self.destination_storage.modified_time(destination)
|
||||
except (OSError, NotImplementedError):
|
||||
# storage doesn't support ``modified_time`` or failed.
|
||||
pass
|
||||
else:
|
||||
destination_is_link= os.path.islink(
|
||||
self.destination_storage.path(destination))
|
||||
if destination_last_modified == source_last_modified:
|
||||
if (not symlink and not destination_is_link):
|
||||
if verbosity >= 2:
|
||||
self.stdout.write("Skipping '%s' (not modified)\n"
|
||||
% destination)
|
||||
self.unmodified_files.append(destination)
|
||||
return False
|
||||
if dry_run:
|
||||
if verbosity >= 2:
|
||||
self.stdout.write("Pretending to delete '%s'\n"
|
||||
% destination)
|
||||
else:
|
||||
if verbosity >= 2:
|
||||
self.stdout.write("Deleting '%s'\n" % destination)
|
||||
self.destination_storage.delete(destination)
|
||||
|
||||
if symlink:
|
||||
destination_path = self.destination_storage.path(destination)
|
||||
if dry_run:
|
||||
if verbosity >= 1:
|
||||
self.stdout.write("Pretending to symlink '%s' to '%s'\n"
|
||||
% (source_path, destination_path))
|
||||
else:
|
||||
if verbosity >= 1:
|
||||
self.stdout.write("Symlinking '%s' to '%s'\n"
|
||||
% (source_path, destination_path))
|
||||
try:
|
||||
os.makedirs(os.path.dirname(destination_path))
|
||||
except OSError:
|
||||
pass
|
||||
os.symlink(source_path, destination_path)
|
||||
self.symlinked_files.append(destination)
|
||||
else:
|
||||
if dry_run:
|
||||
if verbosity >= 1:
|
||||
self.stdout.write("Pretending to copy '%s' to '%s'\n"
|
||||
% (source_path, destination))
|
||||
else:
|
||||
if self.destination_local:
|
||||
destination_path = self.destination_storage.path(destination)
|
||||
try:
|
||||
os.makedirs(os.path.dirname(destination_path))
|
||||
except OSError:
|
||||
pass
|
||||
shutil.copy2(source_path, destination_path)
|
||||
if verbosity >= 1:
|
||||
self.stdout.write("Copying '%s' to '%s'\n"
|
||||
% (source_path, destination_path))
|
||||
else:
|
||||
source_file = source_storage.open(source)
|
||||
self.destination_storage.save(destination, source_file)
|
||||
if verbosity >= 1:
|
||||
self.stdout.write("Copying %s to %s\n"
|
||||
% (source_path, destination))
|
||||
self.copied_files.append(destination)
|
||||
return True
|
|
@ -0,0 +1,24 @@
|
|||
import os
|
||||
from optparse import make_option
|
||||
from django.core.management.base import LabelCommand
|
||||
|
||||
from django.contrib.staticfiles import finders
|
||||
|
||||
class Command(LabelCommand):
|
||||
help = "Finds the absolute paths for the given static file(s)."
|
||||
args = "[file ...]"
|
||||
label = 'static file'
|
||||
option_list = LabelCommand.option_list + (
|
||||
make_option('--first', action='store_false', dest='all', default=True,
|
||||
help="Only return the first match for each static file."),
|
||||
)
|
||||
|
||||
def handle_label(self, path, **options):
|
||||
verbosity = int(options.get('verbosity', 1))
|
||||
result = finders.find(path, all=options['all'])
|
||||
if result:
|
||||
output = '\n '.join((os.path.realpath(path) for path in result))
|
||||
self.stdout.write("Found %r here:\n %s\n" % (path, output))
|
||||
else:
|
||||
if verbosity >= 1:
|
||||
self.stdout.write("No matching file found for %r.\n" % path)
|
|
@ -0,0 +1,84 @@
|
|||
import os
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
from django.contrib.staticfiles import utils
|
||||
|
||||
|
||||
class StaticFilesStorage(FileSystemStorage):
|
||||
"""
|
||||
Standard file system storage for site media files.
|
||||
|
||||
The defaults for ``location`` and ``base_url`` are
|
||||
``STATICFILES_ROOT`` and ``STATICFILES_URL``.
|
||||
"""
|
||||
def __init__(self, location=None, base_url=None, *args, **kwargs):
|
||||
if location is None:
|
||||
location = settings.STATICFILES_ROOT
|
||||
if base_url is None:
|
||||
base_url = settings.STATICFILES_URL
|
||||
if not location:
|
||||
raise ImproperlyConfigured("You're using the staticfiles app "
|
||||
"without having set the STATICFILES_ROOT setting. Set it to "
|
||||
"the absolute path of the directory that holds static media.")
|
||||
if not base_url:
|
||||
raise ImproperlyConfigured("You're using the staticfiles app "
|
||||
"without having set the STATICFILES_URL setting. Set it to "
|
||||
"URL that handles the files served from STATICFILES_ROOT.")
|
||||
super(StaticFilesStorage, self).__init__(location, base_url, *args, **kwargs)
|
||||
|
||||
|
||||
class AppStaticStorage(FileSystemStorage):
|
||||
"""
|
||||
A file system storage backend that takes an app module and works
|
||||
for the ``static`` directory of it.
|
||||
"""
|
||||
source_dir = 'static'
|
||||
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
"""
|
||||
Returns a static file storage if available in the given app.
|
||||
"""
|
||||
# app is actually the models module of the app. Remove the '.models'.
|
||||
bits = app.__name__.split('.')[:-1]
|
||||
self.app_name = bits[-1]
|
||||
self.app_module = '.'.join(bits)
|
||||
# The models module (app) may be a package in which case
|
||||
# dirname(app.__file__) would be wrong. Import the actual app
|
||||
# as opposed to the models module.
|
||||
app = import_module(self.app_module)
|
||||
location = self.get_location(os.path.dirname(app.__file__))
|
||||
super(AppStaticStorage, self).__init__(location, *args, **kwargs)
|
||||
|
||||
def get_location(self, app_root):
|
||||
"""
|
||||
Given the app root, return the location of the static files of an app,
|
||||
by default 'static'. We special case the admin app here since it has
|
||||
its static files in 'media'.
|
||||
"""
|
||||
if self.app_module == 'django.contrib.admin':
|
||||
return os.path.join(app_root, 'media')
|
||||
return os.path.join(app_root, self.source_dir)
|
||||
|
||||
def get_prefix(self):
|
||||
"""
|
||||
Return the path name that should be prepended to files for this app.
|
||||
"""
|
||||
if self.app_module == 'django.contrib.admin':
|
||||
return self.app_name
|
||||
return None
|
||||
|
||||
def get_files(self, ignore_patterns=[]):
|
||||
"""
|
||||
Return a list containing the relative source paths for all files that
|
||||
should be copied for an app.
|
||||
"""
|
||||
files = []
|
||||
prefix = self.get_prefix()
|
||||
for path in utils.get_files(self, ignore_patterns):
|
||||
if prefix:
|
||||
path = '/'.join([prefix, path])
|
||||
files.append(path)
|
||||
return files
|
|
@ -0,0 +1,43 @@
|
|||
from django import template
|
||||
from django.utils.encoding import iri_to_uri
|
||||
|
||||
register = template.Library()
|
||||
|
||||
class StaticFilesPrefixNode(template.Node):
|
||||
|
||||
def __init__(self, varname=None):
|
||||
self.varname = varname
|
||||
|
||||
def render(self, context):
|
||||
try:
|
||||
from django.conf import settings
|
||||
except ImportError:
|
||||
prefix = ''
|
||||
else:
|
||||
prefix = iri_to_uri(settings.STATICFILES_URL)
|
||||
if self.varname is None:
|
||||
return prefix
|
||||
context[self.varname] = prefix
|
||||
return ''
|
||||
|
||||
@register.tag
|
||||
def get_staticfiles_prefix(parser, token):
|
||||
"""
|
||||
Populates a template variable with the prefix (settings.STATICFILES_URL).
|
||||
|
||||
Usage::
|
||||
|
||||
{% get_staticfiles_prefix [as varname] %}
|
||||
|
||||
Examples::
|
||||
|
||||
{% get_staticfiles_prefix %}
|
||||
{% get_staticfiles_prefix as staticfiles_prefix %}
|
||||
|
||||
"""
|
||||
tokens = token.contents.split()
|
||||
if len(tokens) > 1 and tokens[1] != 'as':
|
||||
raise template.TemplateSyntaxError(
|
||||
"First argument in '%s' must be 'as'" % tokens[0])
|
||||
return StaticFilesPrefixNode(varname=(len(tokens) > 1 and tokens[2] or None))
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import re
|
||||
from django.conf import settings
|
||||
from django.conf.urls.defaults import patterns, url, include
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
urlpatterns = []
|
||||
|
||||
# only serve non-fqdn URLs
|
||||
if not settings.DEBUG:
|
||||
urlpatterns += patterns('',
|
||||
url(r'^(?P<path>.*)$', 'django.contrib.staticfiles.views.serve'),
|
||||
)
|
||||
|
||||
def staticfiles_urlpatterns(prefix=None):
|
||||
"""
|
||||
Helper function to return a URL pattern for serving static files.
|
||||
"""
|
||||
if settings.DEBUG:
|
||||
return []
|
||||
if prefix is None:
|
||||
prefix = settings.STATICFILES_URL
|
||||
if '://' in prefix:
|
||||
raise ImproperlyConfigured(
|
||||
"The STATICFILES_URL setting is a full URL, not a path and "
|
||||
"can't be used with the urls.staticfiles_urlpatterns() helper.")
|
||||
if prefix.startswith("/"):
|
||||
prefix = prefix[1:]
|
||||
return patterns('',
|
||||
url(r'^%s' % re.escape(prefix), include(urlpatterns)),)
|
|
@ -0,0 +1,30 @@
|
|||
import fnmatch
|
||||
|
||||
def get_files(storage, ignore_patterns=[], location=''):
|
||||
"""
|
||||
Recursively walk the storage directories gathering a complete list of files
|
||||
that should be copied, returning this list.
|
||||
|
||||
"""
|
||||
def is_ignored(path):
|
||||
"""
|
||||
Return True or False depending on whether the ``path`` should be
|
||||
ignored (if it matches any pattern in ``ignore_patterns``).
|
||||
|
||||
"""
|
||||
for pattern in ignore_patterns:
|
||||
if fnmatch.fnmatchcase(path, pattern):
|
||||
return True
|
||||
return False
|
||||
|
||||
directories, files = storage.listdir(location)
|
||||
static_files = [location and '/'.join([location, fn]) or fn
|
||||
for fn in files
|
||||
if not is_ignored(fn)]
|
||||
for dir in directories:
|
||||
if is_ignored(dir):
|
||||
continue
|
||||
if location:
|
||||
dir = '/'.join([location, dir])
|
||||
static_files.extend(get_files(storage, ignore_patterns, dir))
|
||||
return static_files
|
|
@ -0,0 +1,159 @@
|
|||
"""
|
||||
Views and functions for serving static files. These are only to be used during
|
||||
development, and SHOULD NOT be used in a production setting.
|
||||
|
||||
"""
|
||||
import mimetypes
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import stat
|
||||
import urllib
|
||||
from email.Utils import parsedate_tz, mktime_tz
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified
|
||||
from django.template import loader, Template, Context, TemplateDoesNotExist
|
||||
from django.utils.http import http_date
|
||||
|
||||
from django.contrib.staticfiles import finders
|
||||
|
||||
|
||||
def serve(request, path, document_root=None, show_indexes=False):
|
||||
"""
|
||||
Serve static files below a given point in the directory structure or
|
||||
from locations inferred from the static files finders.
|
||||
|
||||
To use, put a URL pattern such as::
|
||||
|
||||
(r'^(?P<path>.*)$', 'django.contrib.staticfiles.views.serve')
|
||||
|
||||
in your URLconf.
|
||||
|
||||
If you provide the ``document_root`` parameter, the file won't be looked
|
||||
up with the staticfiles finders, but in the given filesystem path, e.g.::
|
||||
|
||||
(r'^(?P<path>.*)$', 'django.contrib.staticfiles.views.serve', {'document_root' : '/path/to/my/files/'})
|
||||
|
||||
You may also set ``show_indexes`` to ``True`` if you'd like to serve a
|
||||
basic index of the directory. This index view will use the
|
||||
template hardcoded below, but if you'd like to override it, you can create
|
||||
a template called ``static/directory_index.html``.
|
||||
"""
|
||||
if settings.DEBUG:
|
||||
raise ImproperlyConfigured("The view to serve static files can only "
|
||||
"be used if the DEBUG setting is True")
|
||||
if not document_root:
|
||||
absolute_path = finders.find(path)
|
||||
if not absolute_path:
|
||||
raise Http404("%r could not be matched to a static file." % path)
|
||||
document_root, path = os.path.split(absolute_path)
|
||||
# Clean up given path to only allow serving files below document_root.
|
||||
path = posixpath.normpath(urllib.unquote(path))
|
||||
path = path.lstrip('/')
|
||||
newpath = ''
|
||||
for part in path.split('/'):
|
||||
if not part:
|
||||
# Strip empty path components.
|
||||
continue
|
||||
drive, part = os.path.splitdrive(part)
|
||||
head, part = os.path.split(part)
|
||||
if part in (os.curdir, os.pardir):
|
||||
# Strip '.' and '..' in path.
|
||||
continue
|
||||
newpath = os.path.join(newpath, part).replace('\\', '/')
|
||||
if newpath and path != newpath:
|
||||
return HttpResponseRedirect(newpath)
|
||||
fullpath = os.path.join(document_root, newpath)
|
||||
if os.path.isdir(fullpath):
|
||||
if show_indexes:
|
||||
return directory_index(newpath, fullpath)
|
||||
raise Http404("Directory indexes are not allowed here.")
|
||||
if not os.path.exists(fullpath):
|
||||
raise Http404('"%s" does not exist' % fullpath)
|
||||
# Respect the If-Modified-Since header.
|
||||
statobj = os.stat(fullpath)
|
||||
mimetype, encoding = mimetypes.guess_type(fullpath)
|
||||
mimetype = mimetype or 'application/octet-stream'
|
||||
if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
|
||||
statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]):
|
||||
return HttpResponseNotModified(mimetype=mimetype)
|
||||
contents = open(fullpath, 'rb').read()
|
||||
response = HttpResponse(contents, mimetype=mimetype)
|
||||
response["Last-Modified"] = http_date(statobj[stat.ST_MTIME])
|
||||
response["Content-Length"] = len(contents)
|
||||
if encoding:
|
||||
response["Content-Encoding"] = encoding
|
||||
return response
|
||||
|
||||
|
||||
DEFAULT_DIRECTORY_INDEX_TEMPLATE = """
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en-us" />
|
||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||
<title>Index of {{ directory }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Index of {{ directory }}</h1>
|
||||
<ul>
|
||||
{% ifnotequal directory "/" %}
|
||||
<li><a href="../">../</a></li>
|
||||
{% endifnotequal %}
|
||||
{% for f in file_list %}
|
||||
<li><a href="{{ f|urlencode }}">{{ f }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def directory_index(path, fullpath):
|
||||
try:
|
||||
t = loader.select_template(['static/directory_index.html',
|
||||
'static/directory_index'])
|
||||
except TemplateDoesNotExist:
|
||||
t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template')
|
||||
files = []
|
||||
for f in os.listdir(fullpath):
|
||||
if not f.startswith('.'):
|
||||
if os.path.isdir(os.path.join(fullpath, f)):
|
||||
f += '/'
|
||||
files.append(f)
|
||||
c = Context({
|
||||
'directory' : path + '/',
|
||||
'file_list' : files,
|
||||
})
|
||||
return HttpResponse(t.render(c))
|
||||
|
||||
def was_modified_since(header=None, mtime=0, size=0):
|
||||
"""
|
||||
Was something modified since the user last downloaded it?
|
||||
|
||||
header
|
||||
This is the value of the If-Modified-Since header. If this is None,
|
||||
I'll just return True.
|
||||
|
||||
mtime
|
||||
This is the modification time of the item we're talking about.
|
||||
|
||||
size
|
||||
This is the size of the item we're talking about.
|
||||
"""
|
||||
try:
|
||||
if header is None:
|
||||
raise ValueError
|
||||
matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header,
|
||||
re.IGNORECASE)
|
||||
header_mtime = mktime_tz(parsedate_tz(matches.group(1)))
|
||||
header_len = matches.group(3)
|
||||
if header_len and int(header_len) != size:
|
||||
raise ValueError
|
||||
if mtime > header_mtime:
|
||||
raise ValueError
|
||||
except (AttributeError, ValueError, OverflowError):
|
||||
return True
|
||||
return False
|
|
@ -71,7 +71,15 @@ def media(request):
|
|||
Adds media-related context variables to the context.
|
||||
|
||||
"""
|
||||
return {'MEDIA_URL': settings.MEDIA_URL}
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"The context processor at `django.core.context_processors.media` is " \
|
||||
"deprecated; use the path `django.contrib.staticfiles.context_processors.staticfiles` " \
|
||||
"instead.",
|
||||
PendingDeprecationWarning
|
||||
)
|
||||
from django.contrib.staticfiles.context_processors import staticfiles as staticfiles_context_processor
|
||||
return staticfiles_context_processor(request)
|
||||
|
||||
def request(request):
|
||||
return {'request': request}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from django.core.management.base import BaseCommand, CommandError
|
||||
from optparse import make_option
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
|
@ -20,6 +22,7 @@ class Command(BaseCommand):
|
|||
import django
|
||||
from django.core.servers.basehttp import run, AdminMediaHandler, WSGIServerException
|
||||
from django.core.handlers.wsgi import WSGIHandler
|
||||
from django.contrib.staticfiles.handlers import StaticFilesHandler
|
||||
if args:
|
||||
raise CommandError('Usage is runserver %s' % self.args)
|
||||
if not addrport:
|
||||
|
@ -56,7 +59,10 @@ class Command(BaseCommand):
|
|||
translation.activate(settings.LANGUAGE_CODE)
|
||||
|
||||
try:
|
||||
handler = AdminMediaHandler(WSGIHandler(), admin_media_path)
|
||||
handler = WSGIHandler()
|
||||
handler = StaticFilesHandler(handler)
|
||||
# serve admin media like old-school (deprecation pending)
|
||||
handler = AdminMediaHandler(handler, admin_media_path)
|
||||
run(addr, int(port), handler)
|
||||
except WSGIServerException, e:
|
||||
# Use helpful error messages instead of ugly tracebacks.
|
||||
|
|
|
@ -8,16 +8,17 @@ been reviewed for security issues. Don't use it for production use.
|
|||
"""
|
||||
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
import urllib
|
||||
import warnings
|
||||
|
||||
from django.core.management.color import color_style
|
||||
from django.utils.http import http_date
|
||||
from django.utils._os import safe_join
|
||||
from django.contrib.staticfiles.handlers import StaticFilesHandler
|
||||
from django.views import static
|
||||
|
||||
__version__ = "0.1"
|
||||
__all__ = ['WSGIServer','WSGIRequestHandler']
|
||||
|
@ -633,86 +634,46 @@ class WSGIRequestHandler(BaseHTTPRequestHandler):
|
|||
|
||||
sys.stderr.write(msg)
|
||||
|
||||
class AdminMediaHandler(object):
|
||||
|
||||
class AdminMediaHandler(StaticFilesHandler):
|
||||
"""
|
||||
WSGI middleware that intercepts calls to the admin media directory, as
|
||||
defined by the ADMIN_MEDIA_PREFIX setting, and serves those images.
|
||||
Use this ONLY LOCALLY, for development! This hasn't been tested for
|
||||
security and is not super efficient.
|
||||
"""
|
||||
def __init__(self, application, media_dir=None):
|
||||
from django.conf import settings
|
||||
self.application = application
|
||||
if not media_dir:
|
||||
|
||||
def get_media_dir(self):
|
||||
import django
|
||||
self.media_dir = \
|
||||
os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
|
||||
else:
|
||||
self.media_dir = media_dir
|
||||
self.media_url = settings.ADMIN_MEDIA_PREFIX
|
||||
return os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
|
||||
|
||||
def get_media_url(self):
|
||||
from django.conf import settings
|
||||
return settings.ADMIN_MEDIA_PREFIX
|
||||
|
||||
def __init__(self, application, media_dir=None):
|
||||
warnings.warn('The AdminMediaHandler handler is deprecated; use the '
|
||||
'`django.contrib.staticfiles.handlers.StaticFilesHandler` instead.',
|
||||
PendingDeprecationWarning)
|
||||
super(AdminMediaHandler, self).__init__(application, media_dir)
|
||||
|
||||
def file_path(self, url):
|
||||
"""
|
||||
Returns the path to the media file on disk for the given URL.
|
||||
|
||||
The passed URL is assumed to begin with ADMIN_MEDIA_PREFIX. If the
|
||||
The passed URL is assumed to begin with ``media_url``. If the
|
||||
resultant file path is outside the media directory, then a ValueError
|
||||
is raised.
|
||||
"""
|
||||
# Remove ADMIN_MEDIA_PREFIX.
|
||||
# Remove ``media_url``.
|
||||
relative_url = url[len(self.media_url):]
|
||||
relative_path = urllib.url2pathname(relative_url)
|
||||
return safe_join(self.media_dir, relative_path)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
import os.path
|
||||
def serve(self, request, path):
|
||||
document_root, path = os.path.split(path)
|
||||
return static.serve(request, path, document_root=document_root)
|
||||
|
||||
# Ignore requests that aren't under ADMIN_MEDIA_PREFIX. Also ignore
|
||||
# all requests if ADMIN_MEDIA_PREFIX isn't a relative URL.
|
||||
if self.media_url.startswith('http://') or self.media_url.startswith('https://') \
|
||||
or not environ['PATH_INFO'].startswith(self.media_url):
|
||||
return self.application(environ, start_response)
|
||||
|
||||
# Find the admin file and serve it up, if it exists and is readable.
|
||||
try:
|
||||
file_path = self.file_path(environ['PATH_INFO'])
|
||||
except ValueError: # Resulting file path was not valid.
|
||||
status = '404 NOT FOUND'
|
||||
headers = {'Content-type': 'text/plain'}
|
||||
output = ['Page not found: %s' % environ['PATH_INFO']]
|
||||
start_response(status, headers.items())
|
||||
return output
|
||||
if not os.path.exists(file_path):
|
||||
status = '404 NOT FOUND'
|
||||
headers = {'Content-type': 'text/plain'}
|
||||
output = ['Page not found: %s' % environ['PATH_INFO']]
|
||||
else:
|
||||
try:
|
||||
fp = open(file_path, 'rb')
|
||||
except IOError:
|
||||
status = '401 UNAUTHORIZED'
|
||||
headers = {'Content-type': 'text/plain'}
|
||||
output = ['Permission denied: %s' % environ['PATH_INFO']]
|
||||
else:
|
||||
# This is a very simple implementation of conditional GET with
|
||||
# the Last-Modified header. It makes media files a bit speedier
|
||||
# because the files are only read off disk for the first
|
||||
# request (assuming the browser/client supports conditional
|
||||
# GET).
|
||||
mtime = http_date(os.stat(file_path)[stat.ST_MTIME])
|
||||
headers = {'Last-Modified': mtime}
|
||||
if environ.get('HTTP_IF_MODIFIED_SINCE', None) == mtime:
|
||||
status = '304 NOT MODIFIED'
|
||||
output = []
|
||||
else:
|
||||
status = '200 OK'
|
||||
mime_type = mimetypes.guess_type(file_path)[0]
|
||||
if mime_type:
|
||||
headers['Content-Type'] = mime_type
|
||||
output = [fp.read()]
|
||||
fp.close()
|
||||
start_response(status, headers.items())
|
||||
return output
|
||||
|
||||
def run(addr, port, wsgi_handler):
|
||||
server_address = (addr, port)
|
||||
|
|
|
@ -9,6 +9,7 @@ import posixpath
|
|||
import re
|
||||
import stat
|
||||
import urllib
|
||||
import warnings
|
||||
from email.Utils import parsedate_tz, mktime_tz
|
||||
|
||||
from django.template import loader
|
||||
|
@ -16,6 +17,10 @@ from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpRespons
|
|||
from django.template import Template, Context, TemplateDoesNotExist
|
||||
from django.utils.http import http_date
|
||||
|
||||
from django.contrib.staticfiles.views import \
|
||||
directory_index, was_modified_since, serve as staticfiles_serve
|
||||
|
||||
|
||||
def serve(request, path, document_root=None, show_indexes=False):
|
||||
"""
|
||||
Serve static files below a given point in the directory structure.
|
||||
|
@ -30,111 +35,7 @@ def serve(request, path, document_root=None, show_indexes=False):
|
|||
but if you'd like to override it, you can create a template called
|
||||
``static/directory_index.html``.
|
||||
"""
|
||||
|
||||
# Clean up given path to only allow serving files below document_root.
|
||||
path = posixpath.normpath(urllib.unquote(path))
|
||||
path = path.lstrip('/')
|
||||
newpath = ''
|
||||
for part in path.split('/'):
|
||||
if not part:
|
||||
# Strip empty path components.
|
||||
continue
|
||||
drive, part = os.path.splitdrive(part)
|
||||
head, part = os.path.split(part)
|
||||
if part in (os.curdir, os.pardir):
|
||||
# Strip '.' and '..' in path.
|
||||
continue
|
||||
newpath = os.path.join(newpath, part).replace('\\', '/')
|
||||
if newpath and path != newpath:
|
||||
return HttpResponseRedirect(newpath)
|
||||
fullpath = os.path.join(document_root, newpath)
|
||||
if os.path.isdir(fullpath):
|
||||
if show_indexes:
|
||||
return directory_index(newpath, fullpath)
|
||||
raise Http404("Directory indexes are not allowed here.")
|
||||
if not os.path.exists(fullpath):
|
||||
raise Http404('"%s" does not exist' % fullpath)
|
||||
# Respect the If-Modified-Since header.
|
||||
statobj = os.stat(fullpath)
|
||||
mimetype, encoding = mimetypes.guess_type(fullpath)
|
||||
mimetype = mimetype or 'application/octet-stream'
|
||||
if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
|
||||
statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]):
|
||||
return HttpResponseNotModified(mimetype=mimetype)
|
||||
contents = open(fullpath, 'rb').read()
|
||||
response = HttpResponse(contents, mimetype=mimetype)
|
||||
response["Last-Modified"] = http_date(statobj[stat.ST_MTIME])
|
||||
response["Content-Length"] = len(contents)
|
||||
if encoding:
|
||||
response["Content-Encoding"] = encoding
|
||||
return response
|
||||
|
||||
DEFAULT_DIRECTORY_INDEX_TEMPLATE = """
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en-us" />
|
||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||
<title>Index of {{ directory }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Index of {{ directory }}</h1>
|
||||
<ul>
|
||||
{% ifnotequal directory "/" %}
|
||||
<li><a href="../">../</a></li>
|
||||
{% endifnotequal %}
|
||||
{% for f in file_list %}
|
||||
<li><a href="{{ f|urlencode }}">{{ f }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def directory_index(path, fullpath):
|
||||
try:
|
||||
t = loader.select_template(['static/directory_index.html',
|
||||
'static/directory_index'])
|
||||
except TemplateDoesNotExist:
|
||||
t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template')
|
||||
files = []
|
||||
for f in os.listdir(fullpath):
|
||||
if not f.startswith('.'):
|
||||
if os.path.isdir(os.path.join(fullpath, f)):
|
||||
f += '/'
|
||||
files.append(f)
|
||||
c = Context({
|
||||
'directory' : path + '/',
|
||||
'file_list' : files,
|
||||
})
|
||||
return HttpResponse(t.render(c))
|
||||
|
||||
def was_modified_since(header=None, mtime=0, size=0):
|
||||
"""
|
||||
Was something modified since the user last downloaded it?
|
||||
|
||||
header
|
||||
This is the value of the If-Modified-Since header. If this is None,
|
||||
I'll just return True.
|
||||
|
||||
mtime
|
||||
This is the modification time of the item we're talking about.
|
||||
|
||||
size
|
||||
This is the size of the item we're talking about.
|
||||
"""
|
||||
try:
|
||||
if header is None:
|
||||
raise ValueError
|
||||
matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header,
|
||||
re.IGNORECASE)
|
||||
header_mtime = mktime_tz(parsedate_tz(matches.group(1)))
|
||||
header_len = matches.group(3)
|
||||
if header_len and int(header_len) != size:
|
||||
raise ValueError
|
||||
if mtime > header_mtime:
|
||||
raise ValueError
|
||||
except (AttributeError, ValueError, OverflowError):
|
||||
return True
|
||||
return False
|
||||
warnings.warn("The view at `django.views.static.serve` is deprecated; "
|
||||
"use the path `django.contrib.staticfiles.views.serve` "
|
||||
"instead.", PendingDeprecationWarning)
|
||||
return staticfiles_serve(request, path, document_root, show_indexes)
|
||||
|
|
|
@ -1,162 +1,399 @@
|
|||
=========================
|
||||
How to serve static files
|
||||
=========================
|
||||
=====================
|
||||
Managing static files
|
||||
=====================
|
||||
|
||||
.. module:: django.views.static
|
||||
:synopsis: Serving of static files during development.
|
||||
.. currentmodule:: django.contrib.staticfiles
|
||||
|
||||
Django itself doesn't serve static (media) files, such as images, style sheets,
|
||||
or video. It leaves that job to whichever Web server you choose.
|
||||
.. versionadded:: 1.3
|
||||
|
||||
The reasoning here is that standard Web servers, such as Apache_, lighttpd_ and
|
||||
Cherokee_, are much more fine-tuned at serving static files than a Web
|
||||
application framework.
|
||||
Django developers mostly concern themselves with the dynamic parts of web
|
||||
applications -- the views and templates that render anew for each request. But
|
||||
web applications have other parts: the static media files (images, CSS,
|
||||
Javascript, etc.) that are needed to render a complete web page.
|
||||
|
||||
With that said, Django does support static files **during development**. You can
|
||||
use the :func:`django.views.static.serve` view to serve media files.
|
||||
For small projects, this isn't a big deal, because you can just keep the media
|
||||
somewhere your web server can find it. However, in bigger projects -- especially
|
||||
those comprised of multiple apps -- dealing with the multiple sets of static
|
||||
files provided by each application starts to get tricky.
|
||||
|
||||
.. _Apache: http://httpd.apache.org/
|
||||
.. _lighttpd: http://www.lighttpd.net/
|
||||
.. _Cherokee: http://www.cherokee-project.com/
|
||||
That's what ``django.contrib.staticfiles`` is for: it collects media from each
|
||||
of your applications (and any other places you specify) into a single location
|
||||
that can easily be served in production.
|
||||
|
||||
.. seealso::
|
||||
.. note::
|
||||
|
||||
If you just need to serve the admin media from a nonstandard location, see
|
||||
the :djadminopt:`--adminmedia` parameter to :djadmin:`runserver`.
|
||||
If you've used the `django-staticfiles`_ third-party app before, then
|
||||
``django.contrib.staticfiles`` will look very familiar. That's because
|
||||
they're essentially the same code: ``django.contrib.staticfiles`` started
|
||||
its life as `django-staticfiles`_ and was merged into Django 1.3.
|
||||
|
||||
The big, fat disclaimer
|
||||
=======================
|
||||
If you're upgrading from ``django-staticfiles``, please see `Upgrading from
|
||||
django-staticfiles`_, below, for a few minor changes you'll need to make.
|
||||
|
||||
Using this method is **inefficient** and **insecure**. Do not use this in a
|
||||
production setting. Use this only for development.
|
||||
.. _django-staticfiles: http://pypi.python.org/pypi/django-staticfiles/
|
||||
|
||||
For information on serving static files in an Apache production environment,
|
||||
see the :ref:`Django mod_wsgi documentation <serving-media-files>`.
|
||||
Using ``django.contrib.staticfiles``
|
||||
====================================
|
||||
|
||||
How to do it
|
||||
============
|
||||
Here's the basic usage in a nutshell:
|
||||
|
||||
Here's the formal definition of the :func:`~django.views.static.serve` view:
|
||||
1. Put your media somewhere that staticfiles will find it..
|
||||
|
||||
.. function:: def serve(request, path, document_root, show_indexes=False)
|
||||
Most of the time this place will be in a ``static`` directory within your
|
||||
application, but it could also be a specific directory you've put into
|
||||
your settings file. See the the documentation for the
|
||||
:setting:`STATICFILES_DIRS` and :setting:`STATICFILES_FINDERS` settings
|
||||
for details on where you can put media.
|
||||
|
||||
To use it, just put this in your :doc:`URLconf </topics/http/urls>`::
|
||||
2. Add some ``staticfiles``-related settings to your settings file.
|
||||
|
||||
(r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
|
||||
{'document_root': '/path/to/media'}),
|
||||
First, you'll need to make sure that ``django.contrib.staticfiles`` is in
|
||||
your :setting:`INSTALLED_APPS`.
|
||||
|
||||
...where ``site_media`` is the URL where your media will be rooted, and
|
||||
``/path/to/media`` is the filesystem root for your media. This will call the
|
||||
:func:`~django.views.static.serve` view, passing in the path from the URLconf
|
||||
and the (required) ``document_root`` parameter.
|
||||
Next, you'll need to edit :setting:`STATICFILES_ROOT` to point to where
|
||||
you'd like your static media stored. For example::
|
||||
|
||||
Given the above URLconf:
|
||||
STATICFILES_ROOT = "/home/jacob/projects/mysite.com/static_media"
|
||||
|
||||
* The file ``/path/to/media/foo.jpg`` will be made available at the URL
|
||||
``/site_media/foo.jpg``.
|
||||
You may also want to set the :setting:`STATICFILES_URL` setting at this
|
||||
time, though the default value (of ``/static/``) is perfect for local
|
||||
development.
|
||||
|
||||
* The file ``/path/to/media/css/mystyles.css`` will be made available
|
||||
at the URL ``/site_media/css/mystyles.css``.
|
||||
There are a number of other options available that let you control *how*
|
||||
media is stored, where ``staticfiles`` searches for files, and how files
|
||||
will be served; see :ref:`the staticfiles settings reference
|
||||
<staticfiles-settings>` for details.
|
||||
|
||||
* The file ``/path/bar.jpg`` will not be accessible, because it doesn't
|
||||
fall under the document root.
|
||||
3. Run the :djadmin:`collectstatic` management command::
|
||||
|
||||
Of course, it's not compulsory to use a fixed string for the
|
||||
``'document_root'`` value. You might wish to make that an entry in your
|
||||
settings file and use the setting value there. That will allow you and
|
||||
other developers working on the code to easily change the value as
|
||||
required. For example, if we have a line in ``settings.py`` that says::
|
||||
./manage.py collectstatic
|
||||
|
||||
STATIC_DOC_ROOT = '/path/to/media'
|
||||
This'll churn through your static file storage and move them into the
|
||||
directory given by :setting:`STATICFILES_ROOT`.
|
||||
|
||||
...we could write the above :doc:`URLconf </topics/http/urls>` entry as::
|
||||
4. Deploy that media.
|
||||
|
||||
from django.conf import settings
|
||||
...
|
||||
(r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
|
||||
{'document_root': settings.STATIC_DOC_ROOT}),
|
||||
If you're using the built-in development server, you can quickly
|
||||
serve static media locally by adding::
|
||||
|
||||
Be careful not to use the same path as your :setting:`ADMIN_MEDIA_PREFIX` (which defaults
|
||||
to ``/media/``) as this will overwrite your URLconf entry.
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
Directory listings
|
||||
==================
|
||||
to the bottom of your URLconf. See :ref:`staticfiles-development` for
|
||||
details.
|
||||
|
||||
Optionally, you can pass the ``show_indexes`` parameter to the
|
||||
:func:`~django.views.static.serve` view. This is ``False`` by default. If it's
|
||||
``True``, Django will display file listings for directories.
|
||||
When it comes time to deploy to production, :ref:`staticfiles-production`
|
||||
covers some common deployment strategies for static files.
|
||||
|
||||
For example::
|
||||
|
||||
(r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
|
||||
{'document_root': '/path/to/media', 'show_indexes': True}),
|
||||
|
||||
You can customize the index view by creating a template called
|
||||
``static/directory_index.html``. That template gets two objects in its context:
|
||||
|
||||
* ``directory`` -- the directory name (a string)
|
||||
* ``file_list`` -- a list of file names (as strings) in the directory
|
||||
|
||||
Here's the default ``static/directory_index.html`` template:
|
||||
However you choose to deploy those files, you'll probably need to refer
|
||||
to them in your templates. The easiest method is to use the included
|
||||
context processor which will allow template code like:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="Content-Language" content="en-us" />
|
||||
<meta name="robots" content="NONE,NOARCHIVE" />
|
||||
<title>Index of {{ directory }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Index of {{ directory }}</h1>
|
||||
<ul>
|
||||
{% for f in file_list %}
|
||||
<li><a href="{{ f }}">{{ f }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
<img src="{{ STATICFILES_URL }}images/hi.jpg />
|
||||
|
||||
.. versionchanged:: 1.0.3
|
||||
Prior to Django 1.0.3, there was a bug in the view that provided directory
|
||||
listings. The template that was loaded had to be called
|
||||
``static/directory_listing`` (with no ``.html`` extension). For backwards
|
||||
compatibility with earlier versions, Django will still load templates with
|
||||
the older (no extension) name, but it will prefer the
|
||||
``directory_index.html`` version.
|
||||
See :ref:`staticfiles-in-templates` for more details, including an
|
||||
alternate method (using a template tag).
|
||||
|
||||
Limiting use to DEBUG=True
|
||||
==========================
|
||||
Those are the basics. For more details on common configuration options, read on;
|
||||
for a detailed reference of the settings, commands, and other bits included with
|
||||
the framework see :doc:`the staticfiles reference </ref/contrib/staticfiles>`.
|
||||
|
||||
Because URLconfs are just plain Python modules, you can use Python logic to
|
||||
make the static-media view available only in development mode. This is a handy
|
||||
trick to make sure the static-serving view doesn't slip into a production
|
||||
setting by mistake.
|
||||
.. _staticfiles-in-templates:
|
||||
|
||||
Do this by wrapping an ``if DEBUG`` statement around the
|
||||
:func:`django.views.static.serve` inclusion. Here's a full example URLconf::
|
||||
Referring to static files in templates
|
||||
======================================
|
||||
|
||||
from django.conf.urls.defaults import *
|
||||
from django.conf import settings
|
||||
At some point, you'll probably need to link to static files in your templates.
|
||||
You could, of course, simply hardcode the path to you assets in the templates:
|
||||
|
||||
urlpatterns = patterns('',
|
||||
(r'^articles/2003/$', 'news.views.special_case_2003'),
|
||||
(r'^articles/(?P<year>\d{4})/$', 'news.views.year_archive'),
|
||||
(r'^articles/(?P<year>\d{4})/(?P<month>\d{2})/$', 'news.views.month_archive'),
|
||||
(r'^articles/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d+)/$', 'news.views.article_detail'),
|
||||
.. code-block:: html
|
||||
|
||||
<img src="http://media.example.com/static/myimage.jpg" />
|
||||
|
||||
Of course, there are some serious problems with this: it doesn't work well in
|
||||
development, and it makes it *very* hard to change where you've deployed your
|
||||
media. If, for example, you wanted to switch to using a content delivery network
|
||||
(CDN), then you'd need to change more or less every single template.
|
||||
|
||||
A far better way is to use the value of the :setting:`STATICFILES_URL` setting
|
||||
directly in your templates. This means that a switch of media servers only
|
||||
requires changing that single value. Much better!
|
||||
|
||||
``staticfiles`` inludes two built-in ways of getting at this setting in your
|
||||
templates: a context processor and a template tag.
|
||||
|
||||
With a context processor
|
||||
------------------------
|
||||
|
||||
The included context processor is the easy way. Simply make sure
|
||||
``'django.contrib.staticfiles.context_processors.staticfiles'`` is in your
|
||||
:setting:`TEMPLATE_CONTEXT_PROCESSORS`. It's there by default, and if you're
|
||||
editing that setting by hand it should look something like::
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
'django.core.context_processors.debug',
|
||||
'django.core.context_processors.i18n',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'django.contrib.staticfiles.context_processors.staticfiles',
|
||||
)
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += patterns('',
|
||||
(r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': '/path/to/media'}),
|
||||
Once that's done, you can refer to :setting:`STATICFILES_URL` in your templates:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
<img src="{{ STATICFILES_URL }}images/hi.jpg />
|
||||
|
||||
If ``{{ STATICFILES_URL }}`` isn't working in your template, you're probably not
|
||||
using :class:`~django.template.RequestContext` when rendering the template.
|
||||
|
||||
As a brief refresher, context processors add variables into the contexts of
|
||||
every template. However, context processors require that you use
|
||||
:class:`~django.template.RequestContext` when rendering templates. This happens
|
||||
automatically if you're using a :doc:`generic view </ref/class-based-views>`,
|
||||
but in views written by hand you'll need to explicitally use ``RequestContext``
|
||||
To see how that works, and to read more details, check out
|
||||
:ref:`subclassing-context-requestcontext`.
|
||||
|
||||
With a template tag
|
||||
-------------------
|
||||
|
||||
The second option is the :ttag:`get_staticfiles_prefix` template tag. You can
|
||||
use this if you're not using :class:`~django.template.RequestContext`, or if you
|
||||
need more control over exactly where and how :setting:`STATICFILES_URL` is
|
||||
injected into the template. Here's an example:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% load staticfiles %}
|
||||
<img src="{% get_staticfiles_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 staticfiles %}
|
||||
{% get_staticfiles_prefix as STATIC_PREFIX %}
|
||||
|
||||
<img src="{{ STATIC_PREFIX }}images/hi.jpg" />
|
||||
<img src="{{ STATIC_PREFIX }}images/hi2.jpg" />
|
||||
|
||||
.. _staticfiles-development:
|
||||
|
||||
Serving static files in development
|
||||
===================================
|
||||
|
||||
The static files tools are mostly designed to help with getting static media
|
||||
successfully deployed into production. This usually means a separate, dedicated
|
||||
media server, which is a lot of overhead to mess with when developing locally.
|
||||
Thus, the ``staticfiles`` app ships with a quick and dirty helper view that you
|
||||
can use to serve media locally in development.
|
||||
|
||||
To enable this view, you'll add a couple of lines to your URLconf. The first
|
||||
line goes at the top of the file, and the last line at the bottom::
|
||||
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
# ... the rest of your URLconf goes here ...
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
This will inspect your :setting:`STATICFILES_URL` and
|
||||
:setting:`STATICFILES_ROOT` settings and wire up the view to serve static media
|
||||
accordingly. Remember to run :djadmin:`collectstatic` when your media changes;
|
||||
the view only serves static files that have been collected.
|
||||
|
||||
.. warning::
|
||||
|
||||
This will only work if :setting:`DEBUG` is ``True``.
|
||||
|
||||
That's because this view is **grossly inefficient** and probably
|
||||
**insecure**. This is only intended for local development, and should
|
||||
**never be used in production**.
|
||||
|
||||
For a few more details, including an alternate method of enabling this view,
|
||||
see :ref:`staticfiles-development-view`.
|
||||
|
||||
.. _staticfiles-production:
|
||||
|
||||
Serving static files in production
|
||||
==================================
|
||||
|
||||
The basic outline of putting static files into production a simple: un the
|
||||
:djadmin:`collectstatic` command when static media changes, then arrange for the
|
||||
collected media directory (:setting:`STATICFILES_ROOT`) to be moved to the media
|
||||
server and served.
|
||||
|
||||
Of course, as with all deployment tasks, the devil's in the details. Every
|
||||
production setup will be a bit different, so you'll need to adapt the basic
|
||||
outline to fit your needs. Below are a few common patterns that might help.
|
||||
|
||||
Serving the app and your static files from the same server
|
||||
----------------------------------------------------------
|
||||
|
||||
If you want to serve your media from the same server that's already serving your
|
||||
app, the basic outline gets modified to look something like:
|
||||
|
||||
* Push your code up to the deployment server.
|
||||
* On the server, run :djadmin:`collectmedia` to move all the media into
|
||||
:setting:`STATICFILES_ROOT`.
|
||||
* Point your web server at :setting:`STATICFILES_ROOT`. For example, here's
|
||||
of :ref:`how to do this under Apache and mod_wsgi <serving-media-files>`.
|
||||
|
||||
You'll probably want to automate this process, especially if you've got multiple
|
||||
web servers. There's any number of ways to do this automation, but one option
|
||||
that many Django developers enjoy is `Fabric`__.
|
||||
|
||||
__ http://fabfile.org/
|
||||
|
||||
Below, and in the following sections, we'll show off a few example fabfiles
|
||||
(i.e. Fabric scripts) that automate these media deployment options. The syntax
|
||||
of a fabfile is fairly streightforward but won't be covered here; consult `Fabric's documentation`__, for a complete explanation of the syntax..
|
||||
|
||||
__ http://docs.fabfile.org/
|
||||
|
||||
So, a fabfile to deploy media to a couple of web servers might look something
|
||||
like::
|
||||
|
||||
from fabric.api import *
|
||||
|
||||
# Hosts to deploy onto
|
||||
env.hosts = ['www1.example.com', 'www2.example.com']
|
||||
|
||||
# Where your project code lives on the server
|
||||
env.project_root = '/home/www/myproject'
|
||||
|
||||
def deploy_static():
|
||||
with cd(env.project_root):
|
||||
run('./manage.py collectstatic')
|
||||
|
||||
Serving static files from a dedicated media server
|
||||
--------------------------------------------------
|
||||
|
||||
Most larger Django apps use a separate Web server -- i.e., one that's not also
|
||||
running Django -- for serving media. This server often runs a different type of
|
||||
web server -- faster but less full-featured. Some good choices are:
|
||||
|
||||
* lighttpd_
|
||||
* Nginx_
|
||||
* TUX_
|
||||
* Cherokee_
|
||||
* A stripped-down version of Apache_
|
||||
|
||||
.. _lighttpd: http://www.lighttpd.net/
|
||||
.. _Nginx: http://wiki.nginx.org/Main
|
||||
.. _TUX: http://en.wikipedia.org/wiki/TUX_web_server
|
||||
.. _Apache: http://httpd.apache.org/
|
||||
.. _Cherokee: http://www.cherokee-project.com/
|
||||
|
||||
Configuring these servers is out of scope of this document; check each server's
|
||||
respective documentation for instructions.
|
||||
|
||||
Since your media server won't be running Django, you'll need to modify the
|
||||
deployment strategy to look something like:
|
||||
|
||||
* When your media changes, run :djadmin:`collectstatic` locally.
|
||||
* Push your local :setting:`STATICFILES_ROOT` up to the media server
|
||||
into the directory that's being served. ``rsync`` is a good
|
||||
choice for this step since it only needs to transfer the
|
||||
bits of static media that have changed.
|
||||
|
||||
Here's how this might look in a fabfile::
|
||||
|
||||
from fabric.api import *
|
||||
from fabric.contrib import project
|
||||
|
||||
# Where the static files get collected locally
|
||||
env.local_static_root = '/tmp/static'
|
||||
|
||||
# Where the static files should go remotely
|
||||
env.remote_static_root = '/home/www/media.example.com'
|
||||
|
||||
@roles('media')
|
||||
def deploy_static():
|
||||
local('./manage.py collectstatic')
|
||||
project.rysnc_project(
|
||||
remote_dir = env.remote_static_root,
|
||||
local_dir = env.local_static_root,
|
||||
delete = True
|
||||
)
|
||||
|
||||
This code is straightforward. It imports the settings and checks the value of
|
||||
the :setting:`DEBUG` setting. If it evaluates to ``True``, then ``site_media``
|
||||
will be associated with the ``django.views.static.serve`` view. If not, then the
|
||||
view won't be made available.
|
||||
.. _staticfiles-from-cdn:
|
||||
|
||||
Of course, the catch here is that you'll have to remember to set ``DEBUG=False``
|
||||
in your production settings file. But you should be doing that anyway.
|
||||
Serving static media from a cloud service or CDN
|
||||
------------------------------------------------
|
||||
|
||||
Another common tactic is to serve media from a cloud storage provider like
|
||||
Amazon's S3__ and/or a CDN (content delivery network). This lets you ignore the
|
||||
problems of serving media, and can often make for faster-loading webpages
|
||||
(especially when using a CDN).
|
||||
|
||||
When using these services, the basic workflow would look a bit like the above,
|
||||
except that instead of using ``rsync`` to transfer your media to the server
|
||||
you'd need to transfer the media to the storage provider or CDN.
|
||||
|
||||
There's any number of ways you might do this, but if the provider has an API a
|
||||
:doc:`custom file storage backend </howto/custom-file-storage>` will make the
|
||||
process incredibly simple. If you've written or are using a 3rd party custom
|
||||
storage backend, you can tell :djadmin:`collectstatic` to use it by setting
|
||||
:setting:`STATICFILES_STORAGE` to the storage engine.
|
||||
|
||||
For example, if you've written an S3 storage backend in
|
||||
``myproject.storage.S3Storage`` you could use it with::
|
||||
|
||||
STATICFILES_STORAGE = 'storages.backends.s3.S3Storage'
|
||||
|
||||
Once that's done, all you have to do is run :djadmin:`collectstatic` and your
|
||||
media would be pushed through your storage package up to S3. If you later needed
|
||||
to swich to a different storage provider, it could be as simple as changing your
|
||||
:setting:`STATICFILES_STORAGE` setting.
|
||||
|
||||
For details on how you'd write one of these backends,
|
||||
:doc:`/howto/custom-file-storage`.
|
||||
|
||||
.. seealso::
|
||||
|
||||
The `django-storages`__ project is a 3rd party app that provides many
|
||||
storage backends for many common file storage APIs (including S3).
|
||||
|
||||
__ http://s3.amazonaws.com/
|
||||
__ http://code.welldev.org/django-storages/wiki/S3Storage
|
||||
|
||||
Upgrading from ``django-staticfiles``
|
||||
=====================================
|
||||
|
||||
``django.contrib.staticfiles`` began its life as `django-staticfiles`_. If
|
||||
you're upgrading from `django-staticfiles`_ to ``django.contrib.staticfiles``,
|
||||
you'll need to make a few changes:
|
||||
|
||||
* Application files should now live in a ``static`` directory in each app
|
||||
(`django-staticfiles`_ used the name ``media``, which was slightly
|
||||
confusing).
|
||||
|
||||
* The management commands ``build_static`` and ``resolve_static`` are now
|
||||
called :djadmin:`collectstatic` and :djadmin:`findstatic`.
|
||||
|
||||
* The settings ``STATIC_URL`` and ``STATIC_ROOT`` were renamed to
|
||||
:setting:`STATICFILES_URL` and :setting:`STATICFILES_ROOT`.
|
||||
|
||||
* The settings ``STATICFILES_PREPEND_LABEL_APPS``,
|
||||
``STATICFILES_MEDIA_DIRNAMES`` and ``STATICFILES_EXCLUDED_APPS`` were
|
||||
removed.
|
||||
|
||||
* The setting ``STATICFILES_RESOLVERS`` was removed, and replaced by the new
|
||||
:setting:`STATICFILES_FINDERS`.
|
||||
|
||||
* The default for :setting:`STATICFILES_STORAGE` was renamed from
|
||||
``staticfiles.storage.StaticFileStorage`` to
|
||||
``staticfiles.storage.StaticFilesStorage``
|
||||
|
||||
Learn more
|
||||
==========
|
||||
|
||||
This document has covered the basics and some common usage patterns. For
|
||||
complete details on all the settings, commands, template tags, and other pieces
|
||||
include in ``django.contrib.staticfiles``, see :doc:`the statcfiles reference
|
||||
</ref/contrib/staticfiles>`.
|
|
@ -155,7 +155,7 @@ The development process
|
|||
:doc:`Apache/mod_python <howto/deployment/modpython>` |
|
||||
:doc:`FastCGI/SCGI/AJP <howto/deployment/fastcgi>` |
|
||||
:doc:`Apache authentication <howto/apache-auth>` |
|
||||
:doc:`Serving static files <howto/static-files>` |
|
||||
:doc:`Handling static files <howto/static-files>` |
|
||||
:doc:`Tracking code errors by e-mail <howto/error-reporting>`
|
||||
|
||||
Other batteries included
|
||||
|
@ -185,6 +185,7 @@ Other batteries included
|
|||
* :doc:`Signals <topics/signals>`
|
||||
* :doc:`Sitemaps <ref/contrib/sitemaps>`
|
||||
* :doc:`Sites <ref/contrib/sites>`
|
||||
* :doc:`Static Files <ref/contrib/staticfiles>`
|
||||
* :doc:`Syndication feeds (RSS/Atom) <ref/contrib/syndication>`
|
||||
* :doc:`Unicode in Django <ref/unicode>`
|
||||
* :doc:`Web design helpers <ref/contrib/webdesign>`
|
||||
|
|
|
@ -38,6 +38,7 @@ those packages have.
|
|||
redirects
|
||||
sitemaps
|
||||
sites
|
||||
staticfiles
|
||||
syndication
|
||||
webdesign
|
||||
|
||||
|
|
|
@ -0,0 +1,283 @@
|
|||
===================
|
||||
The staticfiles app
|
||||
===================
|
||||
|
||||
.. module:: django.contrib.staticfiles
|
||||
:synopsis: An app for handling static files.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
``django.contrib.staticfiles`` collects media from each of your applications
|
||||
(and any other places you specify) into a single location that can easily be
|
||||
served in production.
|
||||
|
||||
.. seealso::
|
||||
|
||||
For an introduction to the static files app and some usage examples, see
|
||||
:doc:`/howto/static-files`.
|
||||
|
||||
.. _staticfiles-settings:
|
||||
|
||||
Settings
|
||||
========
|
||||
|
||||
.. highlight:: python
|
||||
|
||||
The following settings control the behavior of the static files app. Only
|
||||
:setting:`STATICFILES_ROOT` is required, but you'll probably also need to
|
||||
configure :setting:`STATICFILES_URL` as well.
|
||||
|
||||
.. setting:: STATICFILES_ROOT
|
||||
|
||||
STATICFILES_ROOT
|
||||
----------------
|
||||
|
||||
Default: ``''`` (Empty string)
|
||||
|
||||
The absolute path to the directory that holds static files::
|
||||
|
||||
STATICFILES_ROOT = "/home/example.com/static/"
|
||||
|
||||
This is a **required setting** unless you've overridden
|
||||
:setting:`STATICFILES_STORAGE` and are using a custom storage backend.
|
||||
|
||||
.. setting:: STATICFILES_URL
|
||||
|
||||
STATICFILES_URL
|
||||
---------------
|
||||
|
||||
Default: ``'/static/'``
|
||||
|
||||
The URL that handles the files served from :setting:`STATICFILES_ROOT`, e.g.::
|
||||
|
||||
STATICFILES_URL = '/site_media/static/'
|
||||
|
||||
... or perhaps::
|
||||
|
||||
STATICFILES_URL = 'http://media.exmaple.com/'
|
||||
|
||||
This should **always** have a trailing slash.
|
||||
|
||||
.. setting:: STATICFILES_DIRS
|
||||
|
||||
STATICFILES_DIRS
|
||||
----------------
|
||||
|
||||
Default: ``[]``
|
||||
|
||||
This setting defines the additional locations the staticfiles app will traverse
|
||||
if the :class:`FileSystemFinder` finder is enabled, e.g. if you use the
|
||||
:djadmin:`collectstatic` or :djadmin:`findstatic` management command or use the
|
||||
static file serving view.
|
||||
|
||||
It should be defined as a sequence of ``(prefix, path)`` tuples, e.g.::
|
||||
|
||||
STATICFILES_DIRS = (
|
||||
('', '/home/special.polls.com/polls/media'),
|
||||
('', '/home/polls.com/polls/media'),
|
||||
('common', '/opt/webfiles/common'),
|
||||
)
|
||||
|
||||
.. setting:: STATICFILES_STORAGE
|
||||
|
||||
STATICFILES_STORAGE
|
||||
-------------------
|
||||
|
||||
Default: ``'django.contrib.staticfiles.storage.StaticFilesStorage'``
|
||||
|
||||
The file storage engine to use when collecting static files with the
|
||||
:djadmin:`collectstatic` management command.
|
||||
|
||||
For an example, see :ref:`staticfiles-from-cdn`.
|
||||
|
||||
.. setting:: STATICFILES_FINDERS
|
||||
|
||||
STATICFILES_FINDERS
|
||||
-------------------
|
||||
|
||||
Default::
|
||||
|
||||
("django.contrib.staticfiles.finders.FileSystemFinder",
|
||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder")
|
||||
|
||||
The list of finder backends that know how to find static files in
|
||||
various locations.
|
||||
|
||||
The default will find files stored in the :setting:`STATICFILES_DIRS` setting
|
||||
(using :class:`django.contrib.staticfiles.finders.FileSystemFinder`) and in a
|
||||
``static`` subdirectory of each app (using
|
||||
:class:`django.contrib.staticfiles.finders.AppDirectoriesFinder`)
|
||||
|
||||
One finder is disabled by default:
|
||||
:class:`django.contrib.staticfiles.finders.DefaultStorageFinder`. If added to
|
||||
your :setting:`STATICFILES_FINDERS` setting, it will look for static files in
|
||||
the default file storage as defined by the :setting:`DEFAULT_FILE_STORAGE`
|
||||
setting.
|
||||
|
||||
.. note::
|
||||
|
||||
When using the :class:AppDirectoriesFinder` finder, make sure your apps can
|
||||
be found by Django's app loading mechanism. Simply include a ``models``
|
||||
module (an empty ``models.py`` file suffices) and add the app to the
|
||||
:setting:`INSTALLED_APPS` setting of your site.
|
||||
|
||||
Static file finders are currently considered a private interface, and this
|
||||
interface is thus undocumented.
|
||||
|
||||
Management Commands
|
||||
===================
|
||||
|
||||
.. highlight:: console
|
||||
|
||||
``django.contrib.staticfiles`` exposes two management commands.
|
||||
|
||||
collectstatic
|
||||
-------------
|
||||
|
||||
.. django-admin:: collectstatic
|
||||
|
||||
Collects the static files into :setting:`STATICFILES_ROOT`.
|
||||
|
||||
Duplicate file names are resolved in a similar way to how template resolution
|
||||
works: files from apps later in :setting:`INSTALLED_APPS` overwrite those from
|
||||
earlier apps, and files from storage directories later in
|
||||
:setting:`STATICFILES_DIRS` overwrite those from earlier. If you're confused,
|
||||
the :djadmin:`findstatic` command can help show you where
|
||||
|
||||
Files are searched by using the :ref:`enabled finders
|
||||
<staticfiles-finders>`. The default is to look in all locations defined in
|
||||
:ref:`staticfiles-dirs` and in the ``media`` directory of apps specified by the
|
||||
:setting:`INSTALLED_APPS` setting.
|
||||
|
||||
Some commonly used options are:
|
||||
|
||||
``--noinput``
|
||||
Do NOT prompt the user for input of any kind.
|
||||
|
||||
``-i PATTERN`` or ``--ignore=PATTERN``
|
||||
Ignore files or directories matching this glob-style pattern. Use multiple
|
||||
times to ignore more.
|
||||
|
||||
``-n`` or ``--dry-run``
|
||||
Do everything except modify the filesystem.
|
||||
|
||||
``-l`` or ``--link``
|
||||
Create a symbolic link to each file instead of copying.
|
||||
|
||||
``--no-default-ignore``
|
||||
Don't ignore the common private glob-style patterns ``'CVS'``, ``'.*'``
|
||||
and ``'*~'``.
|
||||
|
||||
For a full list of options, refer to the commands own help by running::
|
||||
|
||||
$ python manage.py collectstatic --help
|
||||
|
||||
findstatic
|
||||
----------
|
||||
|
||||
.. django-admin:: findstatic
|
||||
|
||||
Searches for one or more relative paths with the enabled finders.
|
||||
|
||||
For example::
|
||||
|
||||
$ python manage.py findstatic css/base.css admin/js/core.js
|
||||
/home/special.polls.com/core/media/css/base.css
|
||||
/home/polls.com/core/media/css/base.css
|
||||
/home/polls.com/src/django/contrib/admin/media/js/core.js
|
||||
|
||||
By default, all matching locations are found. To only return the first match
|
||||
for each relative path, use the ``--first`` option::
|
||||
|
||||
$ python manage.py findstatic css/base.css --first
|
||||
/home/special.polls.com/core/media/css/base.css
|
||||
|
||||
This is a debugging aid; it'll show you exactly which static file will be
|
||||
collected for a given path.
|
||||
|
||||
Other Helpers
|
||||
=============
|
||||
|
||||
The ``media`` context processor
|
||||
-------------------------------
|
||||
|
||||
.. function:: django.contrib.staticfiles.context_processors.staticfiles
|
||||
|
||||
This context processor adds the :setting:`STATICFILES_URL` into each template
|
||||
context as the variable ``{{ STATICFILES_URL }}``. To use it, make sure that
|
||||
``'django.contrib.staticfiles.context_processors.staticfiles'`` appears
|
||||
somewhere in your :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting.
|
||||
|
||||
Remember, only templates rendered with :class:`~django.template.RequestContext`
|
||||
will have acces to the data provided by this (and any) context processor.
|
||||
|
||||
.. templatetag:: get_staticfiles_prefix
|
||||
|
||||
The ``get_staticfiles_prefix`` templatetag
|
||||
==========================================
|
||||
|
||||
.. highlight:: html+django
|
||||
|
||||
If you're not using :class:`~django.template.RequestContext`, or if you need
|
||||
more control over exactly where and how :setting:`STATICFILES_URL` is injected
|
||||
into the template, you can use the :ttag:`get_staticfiles_prefix` template tag
|
||||
instead::
|
||||
|
||||
{% load staticfiles %}
|
||||
<img src="{% get_staticfiles_prefix %}images/hi.jpg" />
|
||||
|
||||
There's also a second form you can use to avoid extra processing if you need the
|
||||
value multiple times::
|
||||
|
||||
{% load staticfiles %}
|
||||
{% get_staticfiles_prefix as STATIC_PREFIX %}
|
||||
|
||||
<img src="{{ STATIC_PREFIX }}images/hi.jpg" />
|
||||
<img src="{{ STATIC_PREFIX }}images/hi2.jpg" />
|
||||
|
||||
.. _staticfiles-development-view:
|
||||
|
||||
Static file development view
|
||||
----------------------------
|
||||
|
||||
.. highlight:: python
|
||||
|
||||
.. function:: django.contrib.staticfiles.views.serve(request, path)
|
||||
|
||||
This view function serves static media in in development.
|
||||
|
||||
.. warning::
|
||||
|
||||
This view will only work if :setting:`DEBUG` is ``True``.
|
||||
|
||||
That's because this view is **grossly inefficient** and probably
|
||||
**insecure**. This is only intended for local development, and should
|
||||
**never be used in production**.
|
||||
|
||||
To use the view, add the following snippet to the end of your primary URL
|
||||
configuration::
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns = patterns('django.contrib.staticfiles.views',
|
||||
url(r'^static/(?P<path>.*)$', 'serve'),
|
||||
)
|
||||
|
||||
Note, the begin of the pattern (``r'^static/'``) should be your
|
||||
:setting:`STATICFILES_URL` setting.
|
||||
|
||||
Since this is a bit finicky, there's also a helper function that'll do this for you:
|
||||
|
||||
.. function:: django.contrib.staticfiles.urls.staticfiles_urlpatterns()
|
||||
|
||||
This will return the proper URL pattern for serving static files to your
|
||||
already defined pattern list. Use it like this::
|
||||
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
# ... the rest of your URLconf here ...
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
|
|
@ -1482,7 +1482,7 @@ Default::
|
|||
("django.contrib.auth.context_processors.auth",
|
||||
"django.core.context_processors.debug",
|
||||
"django.core.context_processors.i18n",
|
||||
"django.core.context_processors.media",
|
||||
"django.contrib.staticfiles.context_processors.staticfiles",
|
||||
"django.contrib.messages.context_processors.messages")
|
||||
|
||||
A tuple of callables that are used to populate the context in ``RequestContext``.
|
||||
|
|
|
@ -289,6 +289,8 @@ you'll see below.
|
|||
Subclassing Context: RequestContext
|
||||
-----------------------------------
|
||||
|
||||
.. class:: django.template.RequestContext
|
||||
|
||||
Django comes with a special ``Context`` class,
|
||||
``django.template.RequestContext``, that acts slightly differently than the
|
||||
normal ``django.template.Context``. The first difference is that it takes an
|
||||
|
@ -309,7 +311,7 @@ and return a dictionary of items to be merged into the context. By default,
|
|||
("django.contrib.auth.context_processors.auth",
|
||||
"django.core.context_processors.debug",
|
||||
"django.core.context_processors.i18n",
|
||||
"django.core.context_processors.media",
|
||||
"django.contrib.staticfiles.context_processors.staticfiles",
|
||||
"django.contrib.messages.context_processors.messages")
|
||||
|
||||
.. versionadded:: 1.2
|
||||
|
@ -432,6 +434,11 @@ If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every
|
|||
``RequestContext`` will contain a variable ``MEDIA_URL``, providing the
|
||||
value of the :setting:`MEDIA_URL` setting.
|
||||
|
||||
.. versionchanged:: 1.3
|
||||
This context processor has been moved to the new :ref:`staticfiles` app.
|
||||
Please use the new ``django.contrib.staticfiles.context_processors.staticfiles``
|
||||
context processor.
|
||||
|
||||
django.core.context_processors.csrf
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.test import TestCase
|
|||
from django.core.handlers.wsgi import WSGIHandler
|
||||
from django.core.servers.basehttp import AdminMediaHandler
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
class AdminMediaHandlerTests(TestCase):
|
||||
|
||||
|
@ -25,7 +26,7 @@ class AdminMediaHandlerTests(TestCase):
|
|||
"""
|
||||
# Cases that should work on all platforms.
|
||||
data = (
|
||||
('/media/css/base.css', ('css', 'base.css')),
|
||||
('%scss/base.css' % settings.ADMIN_MEDIA_PREFIX, ('css', 'base.css')),
|
||||
)
|
||||
# Cases that should raise an exception.
|
||||
bad_data = ()
|
||||
|
@ -34,19 +35,19 @@ class AdminMediaHandlerTests(TestCase):
|
|||
if os.sep == '/':
|
||||
data += (
|
||||
# URL, tuple of relative path parts.
|
||||
('/media/\\css/base.css', ('\\css', 'base.css')),
|
||||
('%s\\css/base.css' % settings.ADMIN_MEDIA_PREFIX, ('\\css', 'base.css')),
|
||||
)
|
||||
bad_data += (
|
||||
'/media//css/base.css',
|
||||
'/media////css/base.css',
|
||||
'/media/../css/base.css',
|
||||
'%s/css/base.css' % settings.ADMIN_MEDIA_PREFIX,
|
||||
'%s///css/base.css' % settings.ADMIN_MEDIA_PREFIX,
|
||||
'%s../css/base.css' % settings.ADMIN_MEDIA_PREFIX,
|
||||
)
|
||||
elif os.sep == '\\':
|
||||
bad_data += (
|
||||
'/media/C:\css/base.css',
|
||||
'/media//\\css/base.css',
|
||||
'/media/\\css/base.css',
|
||||
'/media/\\\\css/base.css'
|
||||
'%sC:\css/base.css' % settings.ADMIN_MEDIA_PREFIX,
|
||||
'%s/\\css/base.css' % settings.ADMIN_MEDIA_PREFIX,
|
||||
'%s\\css/base.css' % settings.ADMIN_MEDIA_PREFIX,
|
||||
'%s\\\\css/base.css' % settings.ADMIN_MEDIA_PREFIX
|
||||
)
|
||||
for url, path_tuple in data:
|
||||
try:
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
file2 in no_label_app
|
|
@ -0,0 +1 @@
|
|||
File in otherdir.
|
|
@ -0,0 +1 @@
|
|||
This file should be ignored.
|
|
@ -0,0 +1 @@
|
|||
This file should be ignored.
|
|
@ -0,0 +1 @@
|
|||
This file should be ignored.
|
|
@ -0,0 +1 @@
|
|||
In app media directory.
|
|
@ -0,0 +1 @@
|
|||
file1 in the app dir
|
|
@ -0,0 +1 @@
|
|||
This file should be ignored.
|
|
@ -0,0 +1 @@
|
|||
Can we find this file?
|
|
@ -0,0 +1 @@
|
|||
Can we find this file?
|
|
@ -0,0 +1,2 @@
|
|||
In STATICFILES_DIRS directory.
|
||||
|
|
@ -0,0 +1 @@
|
|||
Media file.
|
|
@ -0,0 +1 @@
|
|||
Yeah!
|
|
@ -0,0 +1,330 @@
|
|||
import tempfile
|
||||
import shutil
|
||||
import os
|
||||
import sys
|
||||
import posixpath
|
||||
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.management import call_command
|
||||
from django.db.models.loading import load_app
|
||||
from django.template import Template, Context
|
||||
|
||||
from django.contrib.staticfiles import finders, storage
|
||||
|
||||
TEST_ROOT = os.path.dirname(__file__)
|
||||
|
||||
|
||||
class StaticFilesTestCase(TestCase):
|
||||
"""
|
||||
Test case with a couple utility assertions.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.old_staticfiles_url = settings.STATICFILES_URL
|
||||
self.old_staticfiles_root = settings.STATICFILES_ROOT
|
||||
self.old_staticfiles_dirs = settings.STATICFILES_DIRS
|
||||
self.old_staticfiles_finders = settings.STATICFILES_FINDERS
|
||||
self.old_installed_apps = settings.INSTALLED_APPS
|
||||
self.old_media_root = settings.MEDIA_ROOT
|
||||
self.old_media_url = settings.MEDIA_URL
|
||||
self.old_admin_media_prefix = settings.ADMIN_MEDIA_PREFIX
|
||||
|
||||
# We have to load these apps to test staticfiles.
|
||||
load_app('regressiontests.staticfiles_tests.apps.test')
|
||||
load_app('regressiontests.staticfiles_tests.apps.no_label')
|
||||
site_media = os.path.join(TEST_ROOT, 'project', 'site_media')
|
||||
settings.MEDIA_ROOT = os.path.join(site_media, 'media')
|
||||
settings.MEDIA_URL = '/media/'
|
||||
settings.STATICFILES_ROOT = os.path.join(site_media, 'static')
|
||||
settings.STATICFILES_URL = '/static/'
|
||||
settings.ADMIN_MEDIA_PREFIX = '/static/admin/'
|
||||
settings.STATICFILES_DIRS = (
|
||||
os.path.join(TEST_ROOT, 'project', 'documents'),
|
||||
)
|
||||
settings.STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
'django.contrib.staticfiles.finders.DefaultStorageFinder',
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
settings.MEDIA_ROOT = self.old_media_root
|
||||
settings.MEDIA_URL = self.old_media_url
|
||||
settings.ADMIN_MEDIA_PREFIX = self.old_admin_media_prefix
|
||||
settings.STATICFILES_ROOT = self.old_staticfiles_root
|
||||
settings.STATICFILES_URL = self.old_staticfiles_url
|
||||
settings.STATICFILES_DIRS = self.old_staticfiles_dirs
|
||||
settings.STATICFILES_FINDERS = self.old_staticfiles_finders
|
||||
settings.INSTALLED_APPS = self.old_installed_apps
|
||||
|
||||
def assertFileContains(self, filepath, text):
|
||||
self.failUnless(text in self._get_file(filepath),
|
||||
"'%s' not in '%s'" % (text, filepath))
|
||||
|
||||
def assertFileNotFound(self, filepath):
|
||||
self.assertRaises(IOError, self._get_file, filepath)
|
||||
|
||||
|
||||
class BuildStaticTestCase(StaticFilesTestCase):
|
||||
"""
|
||||
Tests shared by all file-resolving features (collectstatic,
|
||||
findstatic, and static serve view).
|
||||
|
||||
This relies on the asserts defined in UtilityAssertsTestCase, but
|
||||
is separated because some test cases need those asserts without
|
||||
all these tests.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(BuildStaticTestCase, self).setUp()
|
||||
self.old_staticfiles_storage = settings.STATICFILES_STORAGE
|
||||
self.old_root = settings.STATICFILES_ROOT
|
||||
settings.STATICFILES_ROOT = tempfile.mkdtemp()
|
||||
self.run_collectstatic()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(settings.STATICFILES_ROOT)
|
||||
settings.STATICFILES_ROOT = self.old_root
|
||||
super(BuildStaticTestCase, self).tearDown()
|
||||
|
||||
def run_collectstatic(self, **kwargs):
|
||||
call_command('collectstatic', interactive=False, verbosity='0',
|
||||
ignore_patterns=['*.ignoreme'], **kwargs)
|
||||
|
||||
def _get_file(self, filepath):
|
||||
assert filepath, 'filepath is empty.'
|
||||
filepath = os.path.join(settings.STATICFILES_ROOT, filepath)
|
||||
return open(filepath).read()
|
||||
|
||||
|
||||
class TestDefaults(object):
|
||||
"""
|
||||
A few standard test cases.
|
||||
"""
|
||||
def test_staticfiles_dirs(self):
|
||||
"""
|
||||
Can find a file in a STATICFILES_DIRS directory.
|
||||
|
||||
"""
|
||||
self.assertFileContains('test.txt', 'Can we find')
|
||||
|
||||
def test_staticfiles_dirs_subdir(self):
|
||||
"""
|
||||
Can find a file in a subdirectory of a STATICFILES_DIRS
|
||||
directory.
|
||||
|
||||
"""
|
||||
self.assertFileContains('subdir/test.txt', 'Can we find')
|
||||
|
||||
def test_staticfiles_dirs_priority(self):
|
||||
"""
|
||||
File in STATICFILES_DIRS has priority over file in app.
|
||||
|
||||
"""
|
||||
self.assertFileContains('test/file.txt', 'STATICFILES_DIRS')
|
||||
|
||||
def test_app_files(self):
|
||||
"""
|
||||
Can find a file in an app media/ directory.
|
||||
|
||||
"""
|
||||
self.assertFileContains('test/file1.txt', 'file1 in the app dir')
|
||||
|
||||
|
||||
class TestBuildStatic(BuildStaticTestCase, TestDefaults):
|
||||
"""
|
||||
Test ``collectstatic`` management command.
|
||||
"""
|
||||
def test_ignore(self):
|
||||
"""
|
||||
Test that -i patterns are ignored.
|
||||
"""
|
||||
self.assertFileNotFound('test/test.ignoreme')
|
||||
|
||||
def test_common_ignore_patterns(self):
|
||||
"""
|
||||
Common ignore patterns (*~, .*, CVS) are ignored.
|
||||
"""
|
||||
self.assertFileNotFound('test/.hidden')
|
||||
self.assertFileNotFound('test/backup~')
|
||||
self.assertFileNotFound('test/CVS')
|
||||
|
||||
|
||||
class TestBuildStaticExcludeNoDefaultIgnore(BuildStaticTestCase, TestDefaults):
|
||||
"""
|
||||
Test ``--exclude-dirs`` and ``--no-default-ignore`` options for
|
||||
``collectstatic`` management command.
|
||||
"""
|
||||
def run_collectstatic(self):
|
||||
super(TestBuildStaticExcludeNoDefaultIgnore, self).run_collectstatic(
|
||||
use_default_ignore_patterns=False)
|
||||
|
||||
def test_no_common_ignore_patterns(self):
|
||||
"""
|
||||
With --no-default-ignore, common ignore patterns (*~, .*, CVS)
|
||||
are not ignored.
|
||||
|
||||
"""
|
||||
self.assertFileContains('test/.hidden', 'should be ignored')
|
||||
self.assertFileContains('test/backup~', 'should be ignored')
|
||||
self.assertFileContains('test/CVS', 'should be ignored')
|
||||
|
||||
|
||||
class TestBuildStaticDryRun(BuildStaticTestCase):
|
||||
"""
|
||||
Test ``--dry-run`` option for ``collectstatic`` management command.
|
||||
"""
|
||||
def run_collectstatic(self):
|
||||
super(TestBuildStaticDryRun, self).run_collectstatic(dry_run=True)
|
||||
|
||||
def test_no_files_created(self):
|
||||
"""
|
||||
With --dry-run, no files created in destination dir.
|
||||
"""
|
||||
self.assertEquals(os.listdir(settings.STATICFILES_ROOT), [])
|
||||
|
||||
|
||||
if sys.platform != 'win32':
|
||||
class TestBuildStaticLinks(BuildStaticTestCase, TestDefaults):
|
||||
"""
|
||||
Test ``--link`` option for ``collectstatic`` management command.
|
||||
|
||||
Note that by inheriting ``TestDefaults`` we repeat all
|
||||
the standard file resolving tests here, to make sure using
|
||||
``--link`` does not change the file-selection semantics.
|
||||
"""
|
||||
def run_collectstatic(self):
|
||||
super(TestBuildStaticLinks, self).run_collectstatic(link=True)
|
||||
|
||||
def test_links_created(self):
|
||||
"""
|
||||
With ``--link``, symbolic links are created.
|
||||
|
||||
"""
|
||||
self.failUnless(os.path.islink(os.path.join(settings.STATICFILES_ROOT, 'test.txt')))
|
||||
|
||||
|
||||
class TestServeStatic(StaticFilesTestCase):
|
||||
"""
|
||||
Test static asset serving view.
|
||||
"""
|
||||
def _response(self, filepath):
|
||||
return self.client.get(
|
||||
posixpath.join(settings.STATICFILES_URL, filepath))
|
||||
|
||||
def assertFileContains(self, filepath, text):
|
||||
self.assertContains(self._response(filepath), text)
|
||||
|
||||
def assertFileNotFound(self, filepath):
|
||||
self.assertEquals(self._response(filepath).status_code, 404)
|
||||
|
||||
|
||||
class TestServeStaticWithDefaultURL(TestServeStatic, TestDefaults):
|
||||
"""
|
||||
Test static asset serving view with staticfiles_urlpatterns helper.
|
||||
"""
|
||||
urls = "regressiontests.staticfiles_tests.urls.default"
|
||||
|
||||
|
||||
class TestServeStaticWithURLHelper(TestServeStatic, TestDefaults):
|
||||
"""
|
||||
Test static asset serving view with staticfiles_urlpatterns helper.
|
||||
"""
|
||||
urls = "regressiontests.staticfiles_tests.urls.helper"
|
||||
|
||||
|
||||
class TestServeAdminMedia(TestServeStatic):
|
||||
"""
|
||||
Test serving media from django.contrib.admin.
|
||||
"""
|
||||
def _response(self, filepath):
|
||||
return self.client.get(
|
||||
posixpath.join(settings.ADMIN_MEDIA_PREFIX, filepath))
|
||||
|
||||
def test_serve_admin_media(self):
|
||||
self.assertFileContains('css/base.css', 'body')
|
||||
|
||||
|
||||
class FinderTestCase(object):
|
||||
"""
|
||||
Base finder test mixin
|
||||
"""
|
||||
def test_find_first(self):
|
||||
src, dst = self.find_first
|
||||
self.assertEquals(self.finder.find(src), dst)
|
||||
|
||||
def test_find_all(self):
|
||||
src, dst = self.find_all
|
||||
self.assertEquals(self.finder.find(src, all=True), dst)
|
||||
|
||||
|
||||
class TestFileSystemFinder(StaticFilesTestCase, FinderTestCase):
|
||||
"""
|
||||
Test FileSystemFinder.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestFileSystemFinder, self).setUp()
|
||||
self.finder = finders.FileSystemFinder()
|
||||
test_file_path = os.path.join(TEST_ROOT, 'project/documents/test/file.txt')
|
||||
self.find_first = ("test/file.txt", test_file_path)
|
||||
self.find_all = ("test/file.txt", [test_file_path])
|
||||
|
||||
|
||||
class TestAppDirectoriesFinder(StaticFilesTestCase, FinderTestCase):
|
||||
"""
|
||||
Test AppDirectoriesFinder.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestAppDirectoriesFinder, self).setUp()
|
||||
self.finder = finders.AppDirectoriesFinder()
|
||||
test_file_path = os.path.join(TEST_ROOT, 'apps/test/static/test/file1.txt')
|
||||
self.find_first = ("test/file1.txt", test_file_path)
|
||||
self.find_all = ("test/file1.txt", [test_file_path])
|
||||
|
||||
|
||||
class TestDefaultStorageFinder(StaticFilesTestCase, FinderTestCase):
|
||||
"""
|
||||
Test DefaultStorageFinder.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(TestDefaultStorageFinder, self).setUp()
|
||||
self.finder = finders.DefaultStorageFinder(
|
||||
storage=storage.StaticFilesStorage(location=settings.MEDIA_ROOT))
|
||||
test_file_path = os.path.join(settings.MEDIA_ROOT, 'media-file.txt')
|
||||
self.find_first = ("media-file.txt", test_file_path)
|
||||
self.find_all = ("media-file.txt", [test_file_path])
|
||||
|
||||
|
||||
class TestMiscFinder(TestCase):
|
||||
"""
|
||||
A few misc finder tests.
|
||||
"""
|
||||
def test_get_finder(self):
|
||||
self.assertTrue(isinstance(finders.get_finder(
|
||||
"django.contrib.staticfiles.finders.FileSystemFinder"),
|
||||
finders.FileSystemFinder))
|
||||
self.assertRaises(ImproperlyConfigured,
|
||||
finders.get_finder, "django.contrib.staticfiles.finders.FooBarFinder")
|
||||
self.assertRaises(ImproperlyConfigured,
|
||||
finders.get_finder, "foo.bar.FooBarFinder")
|
||||
|
||||
|
||||
class TemplateTagTest(TestCase):
|
||||
def test_get_staticfiles_prefix(self):
|
||||
"""
|
||||
Test the get_staticfiles_prefix helper return the STATICFILES_URL setting.
|
||||
"""
|
||||
self.assertEquals(Template(
|
||||
"{% load staticfiles %}"
|
||||
"{% get_staticfiles_prefix %}"
|
||||
).render(Context()), settings.STATICFILES_URL)
|
||||
|
||||
def test_get_staticfiles_prefix_with_as(self):
|
||||
"""
|
||||
Test the get_staticfiles_prefix helper return the STATICFILES_URL setting.
|
||||
"""
|
||||
self.assertEquals(Template(
|
||||
"{% load staticfiles %}"
|
||||
"{% get_staticfiles_prefix as staticfiles_prefix %}"
|
||||
"{{ staticfiles_prefix }}"
|
||||
).render(Context()), settings.STATICFILES_URL)
|
|
@ -0,0 +1,6 @@
|
|||
from django.conf import settings
|
||||
from django.conf.urls.defaults import *
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^static/(?P<path>.*)$', 'django.contrib.staticfiles.views.serve'),
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
urlpatterns = staticfiles_urlpatterns()
|
|
@ -27,6 +27,7 @@ ALWAYS_INSTALLED_APPS = [
|
|||
'django.contrib.comments',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.admindocs',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
def get_test_models():
|
||||
|
|
|
@ -41,4 +41,7 @@ urlpatterns = patterns('',
|
|||
|
||||
# special headers views
|
||||
(r'special_headers/', include('regressiontests.special_headers.urls')),
|
||||
|
||||
# static files handling
|
||||
(r'^', include('regressiontests.staticfiles_tests.urls.default')),
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue