Browse Source

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
Jannis Leidel 14 years ago
parent
commit
cfc19f84de
54 changed files with 2014 additions and 319 deletions
  1. 32 6
      django/conf/global_settings.py
  2. 25 5
      django/conf/project_template/settings.py
  3. 0 0
      django/contrib/staticfiles/__init__.py
  4. 7 0
      django/contrib/staticfiles/context_processors.py
  5. 254 0
      django/contrib/staticfiles/finders.py
  6. 72 0
      django/contrib/staticfiles/handlers.py
  7. 0 0
      django/contrib/staticfiles/management/__init__.py
  8. 0 0
      django/contrib/staticfiles/management/commands/__init__.py
  9. 184 0
      django/contrib/staticfiles/management/commands/collectstatic.py
  10. 24 0
      django/contrib/staticfiles/management/commands/findstatic.py
  11. 0 0
      django/contrib/staticfiles/models.py
  12. 84 0
      django/contrib/staticfiles/storage.py
  13. 0 0
      django/contrib/staticfiles/templatetags/__init__.py
  14. 43 0
      django/contrib/staticfiles/templatetags/staticfiles.py
  15. 29 0
      django/contrib/staticfiles/urls.py
  16. 30 0
      django/contrib/staticfiles/utils.py
  17. 159 0
      django/contrib/staticfiles/views.py
  18. 9 1
      django/core/context_processors.py
  19. 8 2
      django/core/management/commands/runserver.py
  20. 23 62
      django/core/servers/basehttp.py
  21. 9 108
      django/views/static.py
  22. 360 123
      docs/howto/static-files.txt
  23. 2 1
      docs/index.txt
  24. 1 0
      docs/ref/contrib/index.txt
  25. 283 0
      docs/ref/contrib/staticfiles.txt
  26. 1 1
      docs/ref/settings.txt
  27. 8 1
      docs/ref/templates/api.txt
  28. 10 9
      tests/regressiontests/servers/tests.py
  29. 0 0
      tests/regressiontests/staticfiles_tests/__init__.py
  30. 0 0
      tests/regressiontests/staticfiles_tests/apps/__init__.py
  31. 0 0
      tests/regressiontests/staticfiles_tests/apps/no_label/__init__.py
  32. 0 0
      tests/regressiontests/staticfiles_tests/apps/no_label/models.py
  33. 1 0
      tests/regressiontests/staticfiles_tests/apps/no_label/static/file2.txt
  34. 0 0
      tests/regressiontests/staticfiles_tests/apps/test/__init__.py
  35. 0 0
      tests/regressiontests/staticfiles_tests/apps/test/models.py
  36. 1 0
      tests/regressiontests/staticfiles_tests/apps/test/otherdir/odfile.txt
  37. 1 0
      tests/regressiontests/staticfiles_tests/apps/test/static/test/.hidden
  38. 1 0
      tests/regressiontests/staticfiles_tests/apps/test/static/test/CVS
  39. 1 0
      tests/regressiontests/staticfiles_tests/apps/test/static/test/backup~
  40. 1 0
      tests/regressiontests/staticfiles_tests/apps/test/static/test/file.txt
  41. 1 0
      tests/regressiontests/staticfiles_tests/apps/test/static/test/file1.txt
  42. 1 0
      tests/regressiontests/staticfiles_tests/apps/test/static/test/test.ignoreme
  43. 0 0
      tests/regressiontests/staticfiles_tests/models.py
  44. 1 0
      tests/regressiontests/staticfiles_tests/project/documents/subdir/test.txt
  45. 1 0
      tests/regressiontests/staticfiles_tests/project/documents/test.txt
  46. 2 0
      tests/regressiontests/staticfiles_tests/project/documents/test/file.txt
  47. 1 0
      tests/regressiontests/staticfiles_tests/project/site_media/media/media-file.txt
  48. 1 0
      tests/regressiontests/staticfiles_tests/project/site_media/static/test/storage.txt
  49. 330 0
      tests/regressiontests/staticfiles_tests/tests.py
  50. 0 0
      tests/regressiontests/staticfiles_tests/urls/__init__.py
  51. 6 0
      tests/regressiontests/staticfiles_tests/urls/default.py
  52. 3 0
      tests/regressiontests/staticfiles_tests/urls/helper.py
  53. 1 0
      tests/runtests.py
  54. 3 0
      tests/urls.py

+ 32 - 6
django/conf/global_settings.py

@@ -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/'

+ 25 - 5
django/conf/project_template/settings.py

@@ -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
django/contrib/staticfiles/__init__.py


+ 7 - 0
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,
+    }

+ 254 - 0
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)

+ 72 - 0
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
+

+ 0 - 0
django/contrib/staticfiles/management/__init__.py


+ 0 - 0
django/contrib/staticfiles/management/commands/__init__.py


+ 184 - 0
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

+ 24 - 0
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)

+ 0 - 0
django/contrib/staticfiles/models.py


+ 84 - 0
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

+ 0 - 0
django/contrib/staticfiles/templatetags/__init__.py


+ 43 - 0
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))
+

+ 29 - 0
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)),)

+ 30 - 0
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

+ 159 - 0
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

+ 9 - 1
django/core/context_processors.py

@@ -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}

+ 8 - 2
django/core/management/commands/runserver.py

@@ -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.

+ 23 - 62
django/core/servers/basehttp.py

@@ -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):
+
+    def get_media_dir(self):
+        import django
+        return os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
+
+    def get_media_url(self):
         from django.conf import settings
-        self.application = application
-        if not media_dir:
-            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 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
-
-        # 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)
+    def serve(self, request, path):
+        document_root, path = os.path.split(path)
+        return static.serve(request, path, document_root=document_root)
 
-        # 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 - 108
django/views/static.py

@@ -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)

+ 360 - 123
docs/howto/static-files.txt

@@ -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.
+    
+    If you're upgrading from ``django-staticfiles``, please see `Upgrading from
+    django-staticfiles`_, below, for a few minor changes you'll need to make.
 
-The big, fat disclaimer
-=======================
+.. _django-staticfiles: http://pypi.python.org/pypi/django-staticfiles/
 
-Using this method is **inefficient** and **insecure**. Do not use this in a
-production setting. Use this only for development.
+Using ``django.contrib.staticfiles``
+====================================
 
-For information on serving static files in an Apache production environment,
-see the :ref:`Django mod_wsgi documentation <serving-media-files>`.
+Here's the basic usage in a nutshell:
 
-How to do it
-============
+    1. Put your media somewhere that staticfiles will find it..
 
-Here's the formal definition of the :func:`~django.views.static.serve` view:
+       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.
 
-.. function:: def serve(request, path, document_root, show_indexes=False)
+    2. Add some ``staticfiles``-related settings to your settings file.
 
-To use it, just put this in your :doc:`URLconf </topics/http/urls>`::
+       First, you'll need to make sure that ``django.contrib.staticfiles`` is in
+       your :setting:`INSTALLED_APPS`.
 
-    (r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
-            {'document_root': '/path/to/media'}),
+       Next, you'll need to edit :setting:`STATICFILES_ROOT` to point to where
+       you'd like your static media stored. For example::
 
-...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.
+            STATICFILES_ROOT = "/home/jacob/projects/mysite.com/static_media"
 
-Given the above URLconf:
+       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/foo.jpg`` will be made available at the URL
-      ``/site_media/foo.jpg``.
+       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/to/media/css/mystyles.css`` will be made available
-      at the URL ``/site_media/css/mystyles.css``.
+    3. Run the :djadmin:`collectstatic` management command::
 
-    * The file ``/path/bar.jpg`` will not be accessible, because it doesn't
-      fall under the document root.
+            ./manage.py collectstatic
 
-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::
+       This'll churn through your static file storage and move them into the
+       directory given by :setting:`STATICFILES_ROOT`.
 
-    STATIC_DOC_ROOT = '/path/to/media'
+    4. Deploy that media.
 
-...we could write the above :doc:`URLconf </topics/http/urls>` entry as::
+       If you're using the built-in development server, you can quickly
+       serve static media locally by adding::
 
-    from django.conf import settings
-    ...
-    (r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
-            {'document_root': settings.STATIC_DOC_ROOT}),
+            from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+            urlpatterns += staticfiles_urlpatterns()
 
-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.
+       to the bottom of your URLconf. See :ref:`staticfiles-development` for
+       details.
 
-Directory listings
-==================
+       When it comes time to deploy to production, :ref:`staticfiles-production`
+       covers some common deployment strategies for static files.
 
-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.
+       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:
 
-For example::
+          .. code-block:: html+django
 
-    (r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
-            {'document_root': '/path/to/media', 'show_indexes': True}),
+               <img src="{{ STATICFILES_URL }}images/hi.jpg />
 
-You can customize the index view by creating a template called
-``static/directory_index.html``. That template gets two objects in its context:
+       See :ref:`staticfiles-in-templates` for more details, including an
+       alternate method (using a template tag).
 
-    * ``directory`` -- the directory name (a string)
-    * ``file_list`` -- a list of file names (as strings) in the directory
+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>`.
 
-Here's the default ``static/directory_index.html`` template:
+.. _staticfiles-in-templates:
 
-.. code-block:: html+django
+Referring to static files in templates
+======================================
+
+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:
+
+.. 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.
 
-    <!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>
-
-.. 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.
-
-Limiting use to DEBUG=True
-==========================
-
-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.
-
-Do this by wrapping an ``if DEBUG`` statement around the
-:func:`django.views.static.serve` inclusion. Here's a full example URLconf::
-
-    from django.conf.urls.defaults import *
-    from django.conf import settings
-
-    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'),
+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:
+
+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::
 
-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.
+    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>`.

+ 2 - 1
docs/index.txt

@@ -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>`

+ 1 - 0
docs/ref/contrib/index.txt

@@ -38,6 +38,7 @@ those packages have.
    redirects
    sitemaps
    sites
+   staticfiles
    syndication
    webdesign
 

+ 283 - 0
docs/ref/contrib/staticfiles.txt

@@ -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()
+
+

+ 1 - 1
docs/ref/settings.txt

@@ -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``.

+ 8 - 1
docs/ref/templates/api.txt

@@ -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
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 10 - 9
tests/regressiontests/servers/tests.py

@@ -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
tests/regressiontests/staticfiles_tests/__init__.py


+ 0 - 0
tests/regressiontests/staticfiles_tests/apps/__init__.py


+ 0 - 0
tests/regressiontests/staticfiles_tests/apps/no_label/__init__.py


+ 0 - 0
tests/regressiontests/staticfiles_tests/apps/no_label/models.py


+ 1 - 0
tests/regressiontests/staticfiles_tests/apps/no_label/static/file2.txt

@@ -0,0 +1 @@
+file2 in no_label_app

+ 0 - 0
tests/regressiontests/staticfiles_tests/apps/test/__init__.py


+ 0 - 0
tests/regressiontests/staticfiles_tests/apps/test/models.py


+ 1 - 0
tests/regressiontests/staticfiles_tests/apps/test/otherdir/odfile.txt

@@ -0,0 +1 @@
+File in otherdir.

+ 1 - 0
tests/regressiontests/staticfiles_tests/apps/test/static/test/.hidden

@@ -0,0 +1 @@
+This file should be ignored.

+ 1 - 0
tests/regressiontests/staticfiles_tests/apps/test/static/test/CVS

@@ -0,0 +1 @@
+This file should be ignored.

+ 1 - 0
tests/regressiontests/staticfiles_tests/apps/test/static/test/backup~

@@ -0,0 +1 @@
+This file should be ignored.

+ 1 - 0
tests/regressiontests/staticfiles_tests/apps/test/static/test/file.txt

@@ -0,0 +1 @@
+In app media directory.

+ 1 - 0
tests/regressiontests/staticfiles_tests/apps/test/static/test/file1.txt

@@ -0,0 +1 @@
+file1 in the app dir

+ 1 - 0
tests/regressiontests/staticfiles_tests/apps/test/static/test/test.ignoreme

@@ -0,0 +1 @@
+This file should be ignored.

+ 0 - 0
tests/regressiontests/staticfiles_tests/models.py


+ 1 - 0
tests/regressiontests/staticfiles_tests/project/documents/subdir/test.txt

@@ -0,0 +1 @@
+Can we find this file?

+ 1 - 0
tests/regressiontests/staticfiles_tests/project/documents/test.txt

@@ -0,0 +1 @@
+Can we find this file?

+ 2 - 0
tests/regressiontests/staticfiles_tests/project/documents/test/file.txt

@@ -0,0 +1,2 @@
+In STATICFILES_DIRS directory.
+

+ 1 - 0
tests/regressiontests/staticfiles_tests/project/site_media/media/media-file.txt

@@ -0,0 +1 @@
+Media file.

+ 1 - 0
tests/regressiontests/staticfiles_tests/project/site_media/static/test/storage.txt

@@ -0,0 +1 @@
+Yeah!

+ 330 - 0
tests/regressiontests/staticfiles_tests/tests.py

@@ -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
tests/regressiontests/staticfiles_tests/urls/__init__.py


+ 6 - 0
tests/regressiontests/staticfiles_tests/urls/default.py

@@ -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'),
+)

+ 3 - 0
tests/regressiontests/staticfiles_tests/urls/helper.py

@@ -0,0 +1,3 @@
+from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+
+urlpatterns = staticfiles_urlpatterns()

+ 1 - 0
tests/runtests.py

@@ -27,6 +27,7 @@ ALWAYS_INSTALLED_APPS = [
     'django.contrib.comments',
     'django.contrib.admin',
     'django.contrib.admindocs',
+    'django.contrib.staticfiles',
 ]
 
 def get_test_models():

+ 3 - 0
tests/urls.py

@@ -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')),
 )