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.contrib.auth.context_processors.auth',
     'django.core.context_processors.debug',
     'django.core.context_processors.debug',
     'django.core.context_processors.i18n',
     'django.core.context_processors.i18n',
-    'django.core.context_processors.media',
+    'django.contrib.staticfiles.context_processors.staticfiles',
 #    'django.core.context_processors.request',
 #    'django.core.context_processors.request',
     'django.contrib.messages.context_processors.messages',
     '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.
 # Output to use in template system for invalid (e.g. misspelled) variables.
 TEMPLATE_STRING_IF_INVALID = ''
 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
 # Default e-mail address to use for various automated correspondence from
 # the site managers.
 # the site managers.
 DEFAULT_FROM_EMAIL = 'webmaster@localhost'
 DEFAULT_FROM_EMAIL = 'webmaster@localhost'
@@ -551,3 +546,34 @@ TEST_DATABASE_COLLATION = None
 
 
 # The list of directories to search for fixtures
 # The list of directories to search for fixtures
 FIXTURE_DIRS = ()
 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
 USE_L10N = True
 
 
 # Absolute path to the directory that holds media.
 # Absolute path to the directory that holds media.
-# Example: "/home/media/media.lawrence.com/"
+# Example: "/home/media/media.lawrence.com/media/"
 MEDIA_ROOT = ''
 MEDIA_ROOT = ''
 
 
 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
 # 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/"
 # Examples: "http://media.lawrence.com", "http://example.com/media/"
 MEDIA_URL = ''
 MEDIA_URL = ''
 
 
-# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# Absolute path to the directory that holds media.
-# trailing slash.
+# Example: "/home/media/media.lawrence.com/static/"
-# Examples: "http://foo.com/media/", "/media/".
+STATICFILES_ROOT = ''
-ADMIN_MEDIA_PREFIX = '/media/'
+
+# 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.
 # Make this unique, and don't share it with anybody.
 SECRET_KEY = ''
 SECRET_KEY = ''
@@ -89,6 +108,7 @@ INSTALLED_APPS = (
     'django.contrib.sessions',
     'django.contrib.sessions',
     'django.contrib.sites',
     'django.contrib.sites',
     'django.contrib.messages',
     'django.contrib.messages',
+    'django.contrib.staticfiles',
     # Uncomment the next line to enable the admin:
     # Uncomment the next line to enable the admin:
     # 'django.contrib.admin',
     # 'django.contrib.admin',
     # Uncomment the next line to enable admin documentation:
     # 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.
     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):
 def request(request):
     return {'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
 from optparse import make_option
 import os
 import os
 import sys
 import sys
+import warnings
+
+from django.core.management.base import BaseCommand, CommandError
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
     option_list = BaseCommand.option_list + (
     option_list = BaseCommand.option_list + (
@@ -20,6 +22,7 @@ class Command(BaseCommand):
         import django
         import django
         from django.core.servers.basehttp import run, AdminMediaHandler, WSGIServerException
         from django.core.servers.basehttp import run, AdminMediaHandler, WSGIServerException
         from django.core.handlers.wsgi import WSGIHandler
         from django.core.handlers.wsgi import WSGIHandler
+        from django.contrib.staticfiles.handlers import StaticFilesHandler
         if args:
         if args:
             raise CommandError('Usage is runserver %s' % self.args)
             raise CommandError('Usage is runserver %s' % self.args)
         if not addrport:
         if not addrport:
@@ -56,7 +59,10 @@ class Command(BaseCommand):
             translation.activate(settings.LANGUAGE_CODE)
             translation.activate(settings.LANGUAGE_CODE)
 
 
             try:
             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)
                 run(addr, int(port), handler)
             except WSGIServerException, e:
             except WSGIServerException, e:
                 # Use helpful error messages instead of ugly tracebacks.
                 # 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
 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
-import mimetypes
 import os
 import os
 import re
 import re
-import stat
 import sys
 import sys
 import urllib
 import urllib
+import warnings
 
 
 from django.core.management.color import color_style
 from django.core.management.color import color_style
 from django.utils.http import http_date
 from django.utils.http import http_date
 from django.utils._os import safe_join
 from django.utils._os import safe_join
+from django.contrib.staticfiles.handlers import StaticFilesHandler
+from django.views import static
 
 
 __version__ = "0.1"
 __version__ = "0.1"
 __all__ = ['WSGIServer','WSGIRequestHandler']
 __all__ = ['WSGIServer','WSGIRequestHandler']
@@ -633,86 +634,46 @@ class WSGIRequestHandler(BaseHTTPRequestHandler):
 
 
         sys.stderr.write(msg)
         sys.stderr.write(msg)
 
 
-class AdminMediaHandler(object):
+
+class AdminMediaHandler(StaticFilesHandler):
     """
     """
     WSGI middleware that intercepts calls to the admin media directory, as
     WSGI middleware that intercepts calls to the admin media directory, as
     defined by the ADMIN_MEDIA_PREFIX setting, and serves those images.
     defined by the ADMIN_MEDIA_PREFIX setting, and serves those images.
     Use this ONLY LOCALLY, for development! This hasn't been tested for
     Use this ONLY LOCALLY, for development! This hasn't been tested for
     security and is not super efficient.
     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
         from django.conf import settings
-        self.application = application
+        return settings.ADMIN_MEDIA_PREFIX
-        if not media_dir:
+
-            import django
+    def __init__(self, application, media_dir=None):
-            self.media_dir = \
+        warnings.warn('The AdminMediaHandler handler is deprecated; use the '
-                os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
+            '`django.contrib.staticfiles.handlers.StaticFilesHandler` instead.',
-        else:
+            PendingDeprecationWarning)
-            self.media_dir = media_dir
+        super(AdminMediaHandler, self).__init__(application, media_dir)
-        self.media_url = settings.ADMIN_MEDIA_PREFIX
 
 
     def file_path(self, url):
     def file_path(self, url):
         """
         """
         Returns the path to the media file on disk for the given 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
         resultant file path is outside the media directory, then a ValueError
         is raised.
         is raised.
         """
         """
-        # Remove ADMIN_MEDIA_PREFIX.
+        # Remove ``media_url``.
         relative_url = url[len(self.media_url):]
         relative_url = url[len(self.media_url):]
         relative_path = urllib.url2pathname(relative_url)
         relative_path = urllib.url2pathname(relative_url)
         return safe_join(self.media_dir, relative_path)
         return safe_join(self.media_dir, relative_path)
 
 
-    def __call__(self, environ, start_response):
+    def serve(self, request, path):
-        import os.path
+        document_root, path = os.path.split(path)
-
+        return static.serve(request, path, document_root=document_root)
-        # Ignore requests that aren't under ADMIN_MEDIA_PREFIX. Also ignore
-        # all requests if ADMIN_MEDIA_PREFIX isn't a relative URL.
-        if self.media_url.startswith('http://') or self.media_url.startswith('https://') \
-            or not environ['PATH_INFO'].startswith(self.media_url):
-            return self.application(environ, start_response)
 
 
-        # Find the admin file and serve it up, if it exists and is readable.
-        try:
-            file_path = self.file_path(environ['PATH_INFO'])
-        except ValueError: # Resulting file path was not valid.
-            status = '404 NOT FOUND'
-            headers = {'Content-type': 'text/plain'}
-            output = ['Page not found: %s' % environ['PATH_INFO']]
-            start_response(status, headers.items())
-            return output
-        if not os.path.exists(file_path):
-            status = '404 NOT FOUND'
-            headers = {'Content-type': 'text/plain'}
-            output = ['Page not found: %s' % environ['PATH_INFO']]
-        else:
-            try:
-                fp = open(file_path, 'rb')
-            except IOError:
-                status = '401 UNAUTHORIZED'
-                headers = {'Content-type': 'text/plain'}
-                output = ['Permission denied: %s' % environ['PATH_INFO']]
-            else:
-                # This is a very simple implementation of conditional GET with
-                # the Last-Modified header. It makes media files a bit speedier
-                # because the files are only read off disk for the first
-                # request (assuming the browser/client supports conditional
-                # GET).
-                mtime = http_date(os.stat(file_path)[stat.ST_MTIME])
-                headers = {'Last-Modified': mtime}
-                if environ.get('HTTP_IF_MODIFIED_SINCE', None) == mtime:
-                    status = '304 NOT MODIFIED'
-                    output = []
-                else:
-                    status = '200 OK'
-                    mime_type = mimetypes.guess_type(file_path)[0]
-                    if mime_type:
-                        headers['Content-Type'] = mime_type
-                    output = [fp.read()]
-                    fp.close()
-        start_response(status, headers.items())
-        return output
 
 
 def run(addr, port, wsgi_handler):
 def run(addr, port, wsgi_handler):
     server_address = (addr, port)
     server_address = (addr, port)

+ 9 - 108
django/views/static.py

@@ -9,6 +9,7 @@ import posixpath
 import re
 import re
 import stat
 import stat
 import urllib
 import urllib
+import warnings
 from email.Utils import parsedate_tz, mktime_tz
 from email.Utils import parsedate_tz, mktime_tz
 
 
 from django.template import loader
 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.template import Template, Context, TemplateDoesNotExist
 from django.utils.http import http_date
 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):
 def serve(request, path, document_root=None, show_indexes=False):
     """
     """
     Serve static files below a given point in the directory structure.
     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
     but if you'd like to override it, you can create a template called
     ``static/directory_index.html``.
     ``static/directory_index.html``.
     """
     """
-
+    warnings.warn("The view at `django.views.static.serve` is deprecated; "
-    # Clean up given path to only allow serving files below document_root.
+                  "use the path `django.contrib.staticfiles.views.serve` "
-    path = posixpath.normpath(urllib.unquote(path))
+                  "instead.", PendingDeprecationWarning)
-    path = path.lstrip('/')
+    return staticfiles_serve(request, path, document_root, show_indexes)
-    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

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

@@ -1,162 +1,399 @@
-=========================
+=====================
-How to serve static files
+Managing static files
-=========================
+=====================
 
 
-.. module:: django.views.static
+.. currentmodule:: django.contrib.staticfiles
-   :synopsis: Serving of static files during development.
 
 
-Django itself doesn't serve static (media) files, such as images, style sheets,
+.. versionadded:: 1.3
-or video. It leaves that job to whichever Web server you choose.
 
 
-The reasoning here is that standard Web servers, such as Apache_, lighttpd_ and
+Django developers mostly concern themselves with the dynamic parts of web
-Cherokee_, are much more fine-tuned at serving static files than a Web
+applications -- the views and templates that render anew for each request. But
-application framework.
+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
+For small projects, this isn't a big deal, because you can just keep the media
-use the :func:`django.views.static.serve` view to serve media files.
+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/
+That's what ``django.contrib.staticfiles`` is for: it collects media from each
-.. _lighttpd: http://www.lighttpd.net/
+of your applications (and any other places you specify) into a single location
-.. _Cherokee: http://www.cherokee-project.com/
+that can easily be served in production.
 
 
-.. seealso::
+.. note::
 
 
-    If you just need to serve the admin media from a nonstandard location, see
+    If you've used the `django-staticfiles`_ third-party app before, then
-    the :djadminopt:`--adminmedia` parameter to :djadmin:`runserver`.
+    ``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
+Using ``django.contrib.staticfiles``
-production setting. Use this only for development.
+====================================
 
 
-For information on serving static files in an Apache production environment,
+Here's the basic usage in a nutshell:
-see the :ref:`Django mod_wsgi documentation <serving-media-files>`.
 
 
-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',
+       Next, you'll need to edit :setting:`STATICFILES_ROOT` to point to where
-            {'document_root': '/path/to/media'}),
+       you'd like your static media stored. For example::
 
 
-...where ``site_media`` is the URL where your media will be rooted, and
+            STATICFILES_ROOT = "/home/jacob/projects/mysite.com/static_media"
-``/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.
 
 
-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
+       There are a number of other options available that let you control *how*
-      ``/site_media/foo.jpg``.
+       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
+    3. Run the :djadmin:`collectstatic` management command::
-      at the URL ``/site_media/css/mystyles.css``.
 
 
-    * The file ``/path/bar.jpg`` will not be accessible, because it doesn't
+            ./manage.py collectstatic
-      fall under the document root.
 
 
-Of course, it's not compulsory to use a fixed string for the
+       This'll churn through your static file storage and move them into the
-``'document_root'`` value. You might wish to make that an entry in your
+       directory given by :setting:`STATICFILES_ROOT`.
-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::
 
 
-    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
+            from django.contrib.staticfiles.urls import staticfiles_urlpatterns
-    ...
+            urlpatterns += staticfiles_urlpatterns()
-    (r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
-            {'document_root': settings.STATIC_DOC_ROOT}),
 
 
-Be careful not to use the same path as your :setting:`ADMIN_MEDIA_PREFIX` (which defaults
+       to the bottom of your URLconf. See :ref:`staticfiles-development` for
-to ``/media/``) as this will overwrite your URLconf entry.
+       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
+       However you choose to deploy those files, you'll probably need to refer
-:func:`~django.views.static.serve` view. This is ``False`` by default. If it's
+       to them in your templates. The easiest method is to use the included
-``True``, Django will display file listings for directories.
+       context processor which will allow template code like:
 
 
-For example::
+          .. code-block:: html+django
 
 
-    (r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
+               <img src="{{ STATICFILES_URL }}images/hi.jpg />
-            {'document_root': '/path/to/media', 'show_indexes': True}),
 
 
-You can customize the index view by creating a template called
+       See :ref:`staticfiles-in-templates` for more details, including an
-``static/directory_index.html``. That template gets two objects in its context:
+       alternate method (using a template tag).
 
 
-    * ``directory`` -- the directory name (a string)
+Those are the basics. For more details on common configuration options, read on;
-    * ``file_list`` -- a list of file names (as strings) in the directory
+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">
+With a context processor
-    <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" />
+The included context processor is the easy way. Simply make sure
-        <meta http-equiv="Content-Language" content="en-us" />
+``'django.contrib.staticfiles.context_processors.staticfiles'`` is in your
-        <meta name="robots" content="NONE,NOARCHIVE" />
+:setting:`TEMPLATE_CONTEXT_PROCESSORS`. It's there by default, and if you're
-        <title>Index of {{ directory }}</title>
+editing that setting by hand it should look something like::
-    </head>
+
-    <body>
+    TEMPLATE_CONTEXT_PROCESSORS = (
-        <h1>Index of {{ directory }}</h1>
+        'django.core.context_processors.debug',
-        <ul>
+        'django.core.context_processors.i18n',
-        {% for f in file_list %}
+        'django.contrib.auth.context_processors.auth',
-        <li><a href="{{ f }}">{{ f }}</a></li>
+        'django.contrib.messages.context_processors.messages',
-        {% endfor %}
+        'django.contrib.staticfiles.context_processors.staticfiles',
-        </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'),
     )
     )
 
 
-    if settings.DEBUG:
+Once that's done, you can refer to :setting:`STATICFILES_URL` in your templates:
-        urlpatterns += patterns('',
+
-            (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': '/path/to/media'}),
+.. 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
+.. _staticfiles-from-cdn:
-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
+Serving static media from a cloud service or CDN
-view won't be made available.
+------------------------------------------------
+
+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``
+    The `django-storages`__ project is a 3rd party app that provides many
-in your production settings file. But you should be doing that anyway.
+    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:`Apache/mod_python <howto/deployment/modpython>` |
       :doc:`FastCGI/SCGI/AJP <howto/deployment/fastcgi>` |
       :doc:`FastCGI/SCGI/AJP <howto/deployment/fastcgi>` |
       :doc:`Apache authentication <howto/apache-auth>` |
       :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>`
       :doc:`Tracking code errors by e-mail <howto/error-reporting>`
 
 
 Other batteries included
 Other batteries included
@@ -185,6 +185,7 @@ Other batteries included
     * :doc:`Signals <topics/signals>`
     * :doc:`Signals <topics/signals>`
     * :doc:`Sitemaps <ref/contrib/sitemaps>`
     * :doc:`Sitemaps <ref/contrib/sitemaps>`
     * :doc:`Sites <ref/contrib/sites>`
     * :doc:`Sites <ref/contrib/sites>`
+    * :doc:`Static Files <ref/contrib/staticfiles>`
     * :doc:`Syndication feeds (RSS/Atom) <ref/contrib/syndication>`
     * :doc:`Syndication feeds (RSS/Atom) <ref/contrib/syndication>`
     * :doc:`Unicode in Django <ref/unicode>`
     * :doc:`Unicode in Django <ref/unicode>`
     * :doc:`Web design helpers <ref/contrib/webdesign>`
     * :doc:`Web design helpers <ref/contrib/webdesign>`

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

@@ -38,6 +38,7 @@ those packages have.
    redirects
    redirects
    sitemaps
    sitemaps
    sites
    sites
+   staticfiles
    syndication
    syndication
    webdesign
    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.contrib.auth.context_processors.auth",
     "django.core.context_processors.debug",
     "django.core.context_processors.debug",
     "django.core.context_processors.i18n",
     "django.core.context_processors.i18n",
-    "django.core.context_processors.media",
+    "django.contrib.staticfiles.context_processors.staticfiles",
     "django.contrib.messages.context_processors.messages")
     "django.contrib.messages.context_processors.messages")
 
 
 A tuple of callables that are used to populate the context in ``RequestContext``.
 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
 Subclassing Context: RequestContext
 -----------------------------------
 -----------------------------------
 
 
+.. class:: django.template.RequestContext
+
 Django comes with a special ``Context`` class,
 Django comes with a special ``Context`` class,
 ``django.template.RequestContext``, that acts slightly differently than the
 ``django.template.RequestContext``, that acts slightly differently than the
 normal ``django.template.Context``. The first difference is that it takes an
 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.contrib.auth.context_processors.auth",
     "django.core.context_processors.debug",
     "django.core.context_processors.debug",
     "django.core.context_processors.i18n",
     "django.core.context_processors.i18n",
-    "django.core.context_processors.media",
+    "django.contrib.staticfiles.context_processors.staticfiles",
     "django.contrib.messages.context_processors.messages")
     "django.contrib.messages.context_processors.messages")
 
 
 .. versionadded:: 1.2
 .. 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
 ``RequestContext`` will contain a variable ``MEDIA_URL``, providing the
 value of the :setting:`MEDIA_URL` setting.
 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
 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.handlers.wsgi import WSGIHandler
 from django.core.servers.basehttp import AdminMediaHandler
 from django.core.servers.basehttp import AdminMediaHandler
 
 
+from django.conf import settings
 
 
 class AdminMediaHandlerTests(TestCase):
 class AdminMediaHandlerTests(TestCase):
 
 
@@ -25,7 +26,7 @@ class AdminMediaHandlerTests(TestCase):
         """
         """
         # Cases that should work on all platforms.
         # Cases that should work on all platforms.
         data = (
         data = (
-            ('/media/css/base.css', ('css', 'base.css')),
+            ('%scss/base.css' % settings.ADMIN_MEDIA_PREFIX, ('css', 'base.css')),
         )
         )
         # Cases that should raise an exception.
         # Cases that should raise an exception.
         bad_data = ()
         bad_data = ()
@@ -34,19 +35,19 @@ class AdminMediaHandlerTests(TestCase):
         if os.sep == '/':
         if os.sep == '/':
             data += (
             data += (
                 # URL, tuple of relative path parts.
                 # 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 += (
             bad_data += (
-                '/media//css/base.css',
+                '%s/css/base.css' % settings.ADMIN_MEDIA_PREFIX,
-                '/media////css/base.css',
+                '%s///css/base.css' % settings.ADMIN_MEDIA_PREFIX,
-                '/media/../css/base.css',
+                '%s../css/base.css' % settings.ADMIN_MEDIA_PREFIX,
             )
             )
         elif os.sep == '\\':
         elif os.sep == '\\':
             bad_data += (
             bad_data += (
-                '/media/C:\css/base.css',
+                '%sC:\css/base.css' % settings.ADMIN_MEDIA_PREFIX,
-                '/media//\\css/base.css',
+                '%s/\\css/base.css' % settings.ADMIN_MEDIA_PREFIX,
-                '/media/\\css/base.css',
+                '%s\\css/base.css' % settings.ADMIN_MEDIA_PREFIX,
-                '/media/\\\\css/base.css'
+                '%s\\\\css/base.css' % settings.ADMIN_MEDIA_PREFIX
             )
             )
         for url, path_tuple in data:
         for url, path_tuple in data:
             try:
             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.comments',
     'django.contrib.admin',
     'django.contrib.admin',
     'django.contrib.admindocs',
     'django.contrib.admindocs',
+    'django.contrib.staticfiles',
 ]
 ]
 
 
 def get_test_models():
 def get_test_models():

+ 3 - 0
tests/urls.py

@@ -41,4 +41,7 @@ urlpatterns = patterns('',
 
 
     # special headers views
     # special headers views
     (r'special_headers/', include('regressiontests.special_headers.urls')),
     (r'special_headers/', include('regressiontests.special_headers.urls')),
+
+    # static files handling
+    (r'^', include('regressiontests.staticfiles_tests.urls.default')),
 )
 )