summaryrefslogtreecommitdiff
path: root/django/contrib/staticfiles
diff options
context:
space:
mode:
authorJannis Leidel <jannis@leidel.info>2010-10-20 01:33:24 +0000
committerJannis Leidel <jannis@leidel.info>2010-10-20 01:33:24 +0000
commitcfc19f84def07fb950ae8789ed0655eae4f66a92 (patch)
tree2c02cc5996a28f0ab900def5d804c961753c86d0 /django/contrib/staticfiles
parenta014ee02888d2fcea6880bef51f143632a60aab3 (diff)
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
Diffstat (limited to 'django/contrib/staticfiles')
-rw-r--r--django/contrib/staticfiles/__init__.py0
-rw-r--r--django/contrib/staticfiles/context_processors.py7
-rw-r--r--django/contrib/staticfiles/finders.py254
-rw-r--r--django/contrib/staticfiles/handlers.py72
-rw-r--r--django/contrib/staticfiles/management/__init__.py0
-rw-r--r--django/contrib/staticfiles/management/commands/__init__.py0
-rw-r--r--django/contrib/staticfiles/management/commands/collectstatic.py184
-rw-r--r--django/contrib/staticfiles/management/commands/findstatic.py24
-rw-r--r--django/contrib/staticfiles/models.py0
-rw-r--r--django/contrib/staticfiles/storage.py84
-rw-r--r--django/contrib/staticfiles/templatetags/__init__.py0
-rw-r--r--django/contrib/staticfiles/templatetags/staticfiles.py43
-rw-r--r--django/contrib/staticfiles/urls.py29
-rw-r--r--django/contrib/staticfiles/utils.py30
-rw-r--r--django/contrib/staticfiles/views.py159
15 files changed, 886 insertions, 0 deletions
diff --git a/django/contrib/staticfiles/__init__.py b/django/contrib/staticfiles/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/contrib/staticfiles/__init__.py
diff --git a/django/contrib/staticfiles/context_processors.py b/django/contrib/staticfiles/context_processors.py
new file mode 100644
index 0000000000..e1c8d8c8ed
--- /dev/null
+++ b/django/contrib/staticfiles/context_processors.py
@@ -0,0 +1,7 @@
+from django.conf import settings
+
+def staticfiles(request):
+ return {
+ 'STATICFILES_URL': settings.STATICFILES_URL,
+ 'MEDIA_URL': settings.MEDIA_URL,
+ }
diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py
new file mode 100644
index 0000000000..7f0a497174
--- /dev/null
+++ b/django/contrib/staticfiles/finders.py
@@ -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)
diff --git a/django/contrib/staticfiles/handlers.py b/django/contrib/staticfiles/handlers.py
new file mode 100644
index 0000000000..8681c55c16
--- /dev/null
+++ b/django/contrib/staticfiles/handlers.py
@@ -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
+
diff --git a/django/contrib/staticfiles/management/__init__.py b/django/contrib/staticfiles/management/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/contrib/staticfiles/management/__init__.py
diff --git a/django/contrib/staticfiles/management/commands/__init__.py b/django/contrib/staticfiles/management/commands/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/contrib/staticfiles/management/commands/__init__.py
diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py
new file mode 100644
index 0000000000..aae6bee10e
--- /dev/null
+++ b/django/contrib/staticfiles/management/commands/collectstatic.py
@@ -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
diff --git a/django/contrib/staticfiles/management/commands/findstatic.py b/django/contrib/staticfiles/management/commands/findstatic.py
new file mode 100644
index 0000000000..0f13277c37
--- /dev/null
+++ b/django/contrib/staticfiles/management/commands/findstatic.py
@@ -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)
diff --git a/django/contrib/staticfiles/models.py b/django/contrib/staticfiles/models.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/contrib/staticfiles/models.py
diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py
new file mode 100644
index 0000000000..87b569af76
--- /dev/null
+++ b/django/contrib/staticfiles/storage.py
@@ -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
diff --git a/django/contrib/staticfiles/templatetags/__init__.py b/django/contrib/staticfiles/templatetags/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/contrib/staticfiles/templatetags/__init__.py
diff --git a/django/contrib/staticfiles/templatetags/staticfiles.py b/django/contrib/staticfiles/templatetags/staticfiles.py
new file mode 100644
index 0000000000..6153f5a385
--- /dev/null
+++ b/django/contrib/staticfiles/templatetags/staticfiles.py
@@ -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))
+
diff --git a/django/contrib/staticfiles/urls.py b/django/contrib/staticfiles/urls.py
new file mode 100644
index 0000000000..131b10266b
--- /dev/null
+++ b/django/contrib/staticfiles/urls.py
@@ -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)),)
diff --git a/django/contrib/staticfiles/utils.py b/django/contrib/staticfiles/utils.py
new file mode 100644
index 0000000000..f5a30befe9
--- /dev/null
+++ b/django/contrib/staticfiles/utils.py
@@ -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
diff --git a/django/contrib/staticfiles/views.py b/django/contrib/staticfiles/views.py
new file mode 100644
index 0000000000..27d499df87
--- /dev/null
+++ b/django/contrib/staticfiles/views.py
@@ -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