Code

Ticket #12323: 12323.3.diff

File 12323.3.diff, 61.7 KB (added by jezdez, 4 years ago)

Caching the file finders.

Line 
1diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
2index 2714bfb..6d156ec 100644
3--- a/django/conf/global_settings.py
4+++ b/django/conf/global_settings.py
5@@ -193,7 +193,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
6     'django.contrib.auth.context_processors.auth',
7     'django.core.context_processors.debug',
8     'django.core.context_processors.i18n',
9-    'django.core.context_processors.media',
10+    'django.contrib.staticfiles.context_processors.media',
11 #    'django.core.context_processors.request',
12     'django.contrib.messages.context_processors.messages',
13 )
14@@ -201,11 +201,6 @@ TEMPLATE_CONTEXT_PROCESSORS = (
15 # Output to use in template system for invalid (e.g. misspelled) variables.
16 TEMPLATE_STRING_IF_INVALID = ''
17 
18-# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
19-# trailing slash.
20-# Examples: "http://foo.com/media/", "/media/".
21-ADMIN_MEDIA_PREFIX = '/media/'
22-
23 # Default e-mail address to use for various automated correspondence from
24 # the site managers.
25 DEFAULT_FROM_EMAIL = 'webmaster@localhost'
26@@ -522,3 +517,35 @@ TEST_DATABASE_COLLATION = None
27 
28 # The list of directories to search for fixtures
29 FIXTURE_DIRS = ()
30+
31+###############
32+# STATICFILES #
33+###############
34+
35+# Absolute path to the directory that holds media.
36+# Example: "/home/media/media.lawrence.com/static/"
37+STATICFILES_ROOT = ''
38+
39+# URL that handles the static files served from STATICFILES_ROOT.
40+# Example: "http://media.lawrence.com/static/"
41+STATICFILES_URL = '/static/'
42+
43+# A tuple of two-tuples with a name and the path of additional directories
44+# which hold static files and should be taken into account during resolving
45+STATICFILES_DIRS = ()
46+
47+# The default file storage backend used during the build process
48+STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage'
49+
50+# List of finder classes that know how to find static files in
51+# various locations.
52+STATICFILES_FINDERS = (
53+    'django.contrib.staticfiles.finders.FileSystemFinder',
54+    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
55+    'django.contrib.staticfiles.finders.StorageFinder',
56+)
57+
58+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
59+# trailing slash.
60+# Examples: "http://foo.com/media/admin/", "/media/admin/".
61+ADMIN_MEDIA_PREFIX = '/static/admin/'
62diff --git a/django/contrib/staticfiles/__init__.py b/django/contrib/staticfiles/__init__.py
63new file mode 100644
64index 0000000..e69de29
65diff --git a/django/contrib/staticfiles/context_processors.py b/django/contrib/staticfiles/context_processors.py
66new file mode 100644
67index 0000000..e162861
68--- /dev/null
69+++ b/django/contrib/staticfiles/context_processors.py
70@@ -0,0 +1,7 @@
71+from django.conf import settings
72+
73+def media(request):
74+    return {
75+        'STATICFILES_URL': settings.STATICFILES_URL,
76+        'MEDIA_URL': settings.MEDIA_URL,
77+    }
78diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py
79new file mode 100644
80index 0000000..9c7cb3b
81--- /dev/null
82+++ b/django/contrib/staticfiles/finders.py
83@@ -0,0 +1,195 @@
84+import os
85+from django.conf import settings
86+from django.db import models
87+from django.core.exceptions import ImproperlyConfigured
88+from django.utils.importlib import import_module
89+from django.core.files.storage import default_storage
90+from django.utils.functional import memoize
91+
92+from django.contrib.staticfiles import utils
93+
94+_finders = {}
95+
96+
97+class BaseFinder(object):
98+    """
99+    A base file finder to be used for custom staticfiles finder classes.
100+
101+    """
102+    def find(self, path, all=False):
103+        """
104+        Given a relative file path this ought to find an
105+        absolute file path.
106+
107+        If the ``all`` parameter is ``False`` (default) only
108+        the first found file path will be returned; if set
109+        to ``True`` a list of all found files paths is returned.
110+        """
111+        raise NotImplementedError("Finder subclasses need to implement find()")
112+
113+
114+class FileSystemFinder(BaseFinder):
115+    """
116+    A static files finder that uses the ``STATICFILES_DIRS`` setting
117+    to locate files.
118+    """
119+    def find(self, path, all=False):
120+        """
121+        Looks for files in the extra media locations
122+        as defined in ``STATICFILES_DIRS``.
123+        """
124+        matches = []
125+        for root in settings.STATICFILES_DIRS:
126+            if isinstance(root, (list, tuple)):
127+                prefix, root = root
128+            else:
129+                prefix = ''
130+            matched_path = self.find_location(root, path, prefix)
131+            if matched_path:
132+                if not all:
133+                    return matched_path
134+                matches.append(matched_path)
135+        return matches
136+
137+    def find_location(self, root, path, prefix=None):
138+        """
139+        Find a requested static file in a location, returning the found
140+        absolute path (or ``None`` if no match).
141+        """
142+        if prefix:
143+            prefix = '%s/' % prefix
144+            if not path.startswith(prefix):
145+                return None
146+            path = path[len(prefix):]
147+        path = os.path.join(root, path)
148+        if os.path.exists(path):
149+            return path
150+
151+
152+class AppDirectoriesFinder(BaseFinder):
153+    """
154+    A static files finder that looks in the ``media`` directory of each app.
155+    """
156+    def find(self, path, all=False):
157+        """
158+        Looks for files in the app directories.
159+        """
160+        matches = []
161+        for app in models.get_apps():
162+            app_matches = self.find_in_app(app, path, all=all)
163+            if app_matches:
164+                if not all:
165+                    return app_matches
166+                matches.extend(app_matches)
167+        return matches
168+
169+    def find_in_app(self, app, path, all=False):
170+        """
171+        Find a requested static file in an app's media locations.
172+
173+        If ``all`` is ``False`` (default), return the first matching
174+        absolute path (or ``None`` if no match). Otherwise return a list of
175+        found absolute paths.
176+
177+        """
178+        prefix = utils.get_app_prefix(app)
179+        if prefix:
180+            prefix = '%s/' % prefix
181+            if not path.startswith(prefix):
182+                return []
183+            path = path[len(prefix):]
184+        paths = []
185+        storage = utils.app_static_storage(app)
186+        if storage and storage.exists(path):
187+            matched_path = storage.path(path)
188+            if not all:
189+                return matched_path
190+            paths.append(matched_path)
191+        return paths
192+
193+
194+class StorageFinder(BaseFinder):
195+    """
196+    A static files finder that uses the default storage backend.
197+    """
198+    static_storage = default_storage
199+
200+    def __init__(self, storage=None, *args, **kwargs):
201+        if storage is not None:
202+            self.static_storage = storage
203+        super(StorageFinder, self).__init__(*args, **kwargs)
204+
205+    def find(self, path, all=False):
206+        """
207+        Last resort, looks for files in the
208+        static files storage if it's local.
209+        """
210+        try:
211+            self.static_storage.path('')
212+        except NotImplementedError:
213+            pass
214+        else:
215+            if self.static_storage.exists(path):
216+                match = self.static_storage.path(path)
217+                if all:
218+                    match = [match]
219+                return match
220+        return []
221+
222+
223+def find(path, all=False):
224+    """
225+    Find a requested static file, first looking in any defined extra media
226+    locations and next in any (non-excluded) installed apps.
227+   
228+    If no matches are found and the static location is local, look for a match
229+    there too.
230+   
231+    If ``all`` is ``False`` (default), return the first matching
232+    absolute path (or ``None`` if no match). Otherwise return a list of
233+    found absolute paths.
234+   
235+    """
236+    matches = []
237+    for finder_path in settings.STATICFILES_FINDERS:
238+        finder = get_finder(finder_path)
239+        result = finder.find(path, all=all)
240+        if not all and result:
241+            return result
242+        if not isinstance(result, (list, tuple)):
243+            result = [result]
244+        matches.extend(result)
245+
246+    if matches:
247+        return matches
248+
249+    # No match.
250+    return all and [] or None
251+
252+
253+def _get_finder(import_path):
254+    """
255+    Imports the message storage class described by import_path, where
256+    import_path is the full Python path to the class.
257+    """
258+    try:
259+        dot = import_path.rindex('.')
260+    except ValueError:
261+        raise ImproperlyConfigured("%s isn't a Python path." % import_path)
262+    module, classname = import_path[:dot], import_path[dot + 1:]
263+    try:
264+        mod = import_module(module)
265+    except ImportError, e:
266+        raise ImproperlyConfigured('Error importing module %s: "%s"' %
267+                                   (module, e))
268+    try:
269+        cls = getattr(mod, classname)
270+    except AttributeError:
271+        raise ImproperlyConfigured('Module "%s" does not define a "%s" '
272+                                   'class.' % (module, classname))
273+    if not issubclass(cls, BaseFinder):
274+        raise ImproperlyConfigured('Finder "%s" is not a subclass of "%s"' %
275+                                   (cls, BaseFinder))
276+    return cls()
277+
278+get_finder = memoize(_get_finder, _finders, 1)
279diff --git a/django/contrib/staticfiles/handlers.py b/django/contrib/staticfiles/handlers.py
280new file mode 100644
281index 0000000..4765591
282--- /dev/null
283+++ b/django/contrib/staticfiles/handlers.py
284@@ -0,0 +1,64 @@
285+import os
286+import urllib
287+
288+
289+from django.core.handlers.wsgi import WSGIHandler, STATUS_CODE_TEXT
290+from django.http import Http404
291+from django.conf import settings
292+from django.views import static
293+
294+class StaticFilesHandler(WSGIHandler):
295+    """
296+    WSGI middleware that intercepts calls to the static files directory, as
297+    defined by the STATICFILES_URL setting, and serves those files.
298+    """
299+    media_dir = settings.STATICFILES_ROOT
300+    media_url = settings.STATICFILES_URL
301+
302+    def __init__(self, application, media_dir=None):
303+        self.application = application
304+        if media_dir:
305+            self.media_dir = media_dir
306+
307+    def file_path(self, url):
308+        """
309+        Returns the relative path to the media file on disk for the given URL.
310+
311+        The passed URL is assumed to begin with ``media_url``.  If the
312+        resultant file path is outside the media directory, then a ValueError
313+        is raised.
314+        """
315+        # Remove ``media_url``.
316+        relative_url = url[len(self.media_url):]
317+        return urllib.url2pathname(relative_url)
318+
319+    def serve(self, request, path):
320+        from django.contrib.staticfiles import finders
321+        absolute_path = finders.find(path)
322+        if not absolute_path:
323+            raise Http404('%r could not be matched to a static file.' % path)
324+        absolute_path, filename = os.path.split(absolute_path)
325+        return static.serve(request, path=filename, document_root=absolute_path)
326+
327+    def __call__(self, environ, start_response):
328+        # Ignore requests that aren't under the media prefix.
329+        # Also ignore all requests if prefix isn't a relative URL.
330+        if (self.media_url.startswith('http://') or
331+            self.media_url.startswith('https://') or
332+                not environ['PATH_INFO'].startswith(self.media_url)):
333+            return self.application(environ, start_response)
334+        request = self.application.request_class(environ)
335+        try:
336+            response = self.serve(request, self.file_path(environ['PATH_INFO']))
337+        except Http404:
338+            status = '404 NOT FOUND'
339+            start_response(status, {'Content-type': 'text/plain'}.items())
340+            return [str('Page not found: %s' % environ['PATH_INFO'])]
341+        status_text = STATUS_CODE_TEXT[response.status_code]
342+        status = '%s %s' % (response.status_code, status_text)
343+        response_headers = [(str(k), str(v)) for k, v in response.items()]
344+        for c in response.cookies.values():
345+            response_headers.append(('Set-Cookie', str(c.output(header=''))))
346+        start_response(status, response_headers)
347+        return response
348+
349diff --git a/django/contrib/staticfiles/management/__init__.py b/django/contrib/staticfiles/management/__init__.py
350new file mode 100644
351index 0000000..e69de29
352diff --git a/django/contrib/staticfiles/management/commands/__init__.py b/django/contrib/staticfiles/management/commands/__init__.py
353new file mode 100644
354index 0000000..1bca2d4
355--- /dev/null
356+++ b/django/contrib/staticfiles/management/commands/__init__.py
357@@ -0,0 +1,3 @@
358+import logging
359+
360+
361diff --git a/django/contrib/staticfiles/management/commands/build_static.py b/django/contrib/staticfiles/management/commands/build_static.py
362new file mode 100644
363index 0000000..bb0b989
364--- /dev/null
365+++ b/django/contrib/staticfiles/management/commands/build_static.py
366@@ -0,0 +1,196 @@
367+import os
368+import sys
369+import shutil
370+from optparse import make_option
371+
372+from django.conf import settings
373+from django.core.files.storage import FileSystemStorage, get_storage_class
374+from django.core.management.base import CommandError, OptionalAppCommand
375+
376+from django.contrib.staticfiles import utils
377+
378+
379+class Command(OptionalAppCommand):
380+    """
381+    Command that allows to copy or symlink media files from different
382+    locations to the settings.STATICFILES_ROOT.
383+    """
384+    option_list = OptionalAppCommand.option_list + (
385+        make_option('--noinput', action='store_false', dest='interactive',
386+            default=True, help="Do NOT prompt the user for input of any "
387+                "kind."),
388+        make_option('-i', '--ignore', action='append', default=[],
389+            dest='ignore_patterns', metavar='PATTERN',
390+            help="Ignore files or directories matching this glob-style "
391+                "pattern. Use multiple times to ignore more."),
392+        make_option('-n', '--dry-run', action='store_true', dest='dry_run',
393+            help="Do everything except modify the filesystem."),
394+        make_option('-l', '--link', action='store_true', dest='link',
395+            help="Create a symbolic link to each file instead of copying."),
396+        make_option('--exclude-dirs', action='store_true',
397+            help="Exclude additional static locations specified in the "
398+                "STATICFILES_DIRS setting."),
399+        make_option('--no-default-ignore', action='store_false',
400+            dest='use_default_ignore_patterns', default=True,
401+            help="Don't ignore the common private glob-style patterns 'CVS', "
402+                "'.*' and '*~'."),
403+    )
404+    help = ("Copy static media files from apps and other locations in a "
405+            "single location.")
406+
407+    def handle(self, *app_labels, **options):
408+        ignore_patterns = options['ignore_patterns']
409+        if options['use_default_ignore_patterns']:
410+            ignore_patterns += ['CVS', '.*', '*~']
411+            options['ignore_patterns'] = ignore_patterns
412+        options['skipped_files'] = []
413+        options['copied_files'] = []
414+        options['symlinked_files'] = []
415+        options['destination_storage'] = get_storage_class(settings.STATICFILES_STORAGE)()
416+        try:
417+            destination_paths = utils.get_files(options['destination_storage'], ignore_patterns)
418+        except OSError:
419+            # The destination storage location may not exist yet. It'll get
420+            # created when the first file is copied.
421+            destination_paths = []
422+        options['destination_paths'] = destination_paths
423+        try:
424+            options['destination_storage'].path('')
425+            destination_local = True
426+        except NotImplementedError:
427+            destination_local = False
428+        options['destination_local'] = destination_local
429+        if options.get('link', False):
430+            if sys.platform == 'win32':
431+                message = "Symlinking is not supported by this platform (%s)."
432+                raise CommandError(message % sys.platform)
433+            if not destination_local:
434+                raise CommandError("Can't symlink to a remote destination.")
435+        # Warn before doing anything more.
436+        if options.get('interactive'):
437+            confirm = raw_input("""
438+You have requested to collate static media files and copy them to the
439+destination location as specified in your settings file.
440+
441+This will overwrite existing files. Are you sure you want to do this?
442+
443+Type 'yes' to continue, or 'no' to cancel: """)
444+            if confirm != 'yes':
445+                raise CommandError("Static files build cancelled.")
446+        return super(Command, self).handle(*app_labels, **options)
447+
448+    def pre_handle_apps(self, **options):
449+        """
450+        Copy all files from a directory.
451+
452+        """
453+        if not options.get('exclude_dirs', False):
454+            ignore_patterns = options['ignore_patterns']
455+            for root in settings.STATICFILES_DIRS:
456+                if isinstance(root, (list, tuple)):
457+                    prefix, root = root
458+                else:
459+                    prefix = ''
460+                source_storage = FileSystemStorage(location=root)
461+                for source in utils.get_files(source_storage, ignore_patterns):
462+                    self.copy_file(source, prefix, source_storage, **options)
463+
464+    def post_handle_apps(self, **options):
465+        copied_files = options['copied_files']
466+        symlinked_files = options['symlinked_files']
467+        logger = self.get_logger()
468+        count = len(copied_files) + len(symlinked_files)
469+        logger.info("%s static file%s built." % (count,
470+                                                 count != 1 and 's' or ''))
471+
472+    def handle_app(self, app, **options):
473+        """
474+        Copy all static media files from an application.
475+       
476+        """
477+        ignore_patterns = options['ignore_patterns']
478+        prefix = utils.get_app_prefix(app)
479+        storage = utils.app_static_storage(app)
480+        if storage:
481+            for path in utils.get_files(storage, ignore_patterns):
482+                self.copy_file(path, prefix, storage, **options)
483+
484+    def excluded_app(self, app, **options):
485+        """
486+        Gather all the static media files for excluded apps.
487+       
488+        This is so a warning can be issued for later copied files which would
489+        normally be copied by excluded apps.
490+       
491+        """
492+        skipped_files = options['skipped_files']
493+        ignore_patterns = options['ignore_patterns']
494+        all_files = utils.get_files_for_app(app, ignore_patterns)
495+        skipped_files.extend(all_files)
496+
497+    def copy_file(self, source, destination_prefix, source_storage, **options):
498+        """
499+        Attempt to copy (or symlink) `source` to `destination`, returning True
500+        if successful.
501+        """
502+        destination_storage = options['destination_storage']
503+        dry_run = options.get('dry_run', False)
504+        logger = self.get_logger()
505+        if destination_prefix:
506+            destination = '/'.join([destination_prefix, source])
507+        else:
508+            destination = source
509+
510+        if destination in options['copied_files']:
511+            logger.warning("Skipping duplicate file (already copied earlier):"
512+                           "\n  %s" % destination)
513+            return False
514+        if destination in options['symlinked_files']:
515+            logger.warning("Skipping duplicate file (already linked earlier):"
516+                           "\n  %s" % destination)
517+            return False
518+        if destination in options['skipped_files']:
519+            logger.warning("Copying file that would normally be provided by "
520+                           "an excluded application:\n  %s" % destination)
521+        source_path = source_storage.path(source)
522+        if destination in options['destination_paths']:
523+            if dry_run:
524+                logger.info("Pretending to delete:\n  %s" % destination)
525+            else:
526+                logger.debug("Deleting:\n  %s" % destination)
527+                destination_storage.delete(destination)
528+        if options.get('link', False):
529+            destination_path = destination_storage.path(destination)
530+            if dry_run:
531+                logger.info("Pretending to symlink:\n  %s\nto:\n  %s"
532+                            % (source_path, destination_path))
533+            else:
534+                logger.debug("Symlinking:\n  %s\nto:\n  %s"
535+                             % (source_path, destination_path))
536+                try:
537+                    os.makedirs(os.path.dirname(destination_path))
538+                except OSError:
539+                    pass
540+                os.symlink(source_path, destination_path)
541+            options['symlinked_files'].append(destination)
542+        else:
543+            if dry_run:
544+                logger.info("Pretending to copy:\n  %s\nto:\n  %s"
545+                            % (source_path, destination))
546+            else:
547+                if options['destination_local']:
548+                    destination_path = destination_storage.path(destination)
549+                    try:
550+                        os.makedirs(os.path.dirname(destination_path))
551+                    except OSError:
552+                        pass
553+                    shutil.copy2(source_path, destination_path)
554+                    logger.debug("Copying:\n  %s\nto:\n  %s"
555+                                 % (source_path, destination_path))
556+                else:
557+                    source_file = source_storage.open(source)
558+                    destination_storage.write(destination, source_file)
559+                    logger.debug("Copying:\n  %s\nto:\n  %s"
560+                                 % (source_path, destination))
561+            options['copied_files'].append(destination)
562+        return True
563diff --git a/django/contrib/staticfiles/management/commands/find_static.py b/django/contrib/staticfiles/management/commands/find_static.py
564new file mode 100644
565index 0000000..9fdff6b
566--- /dev/null
567+++ b/django/contrib/staticfiles/management/commands/find_static.py
568@@ -0,0 +1,24 @@
569+import os
570+from optparse import make_option
571+from django.core.management.base import LabelCommand
572+
573+from django.contrib.staticfiles import finders
574+
575+class Command(LabelCommand):
576+    help = "Finds the absolute paths for the given static file(s)."
577+    args = "[static_file ...]"
578+    label = 'static file'
579+    option_list = LabelCommand.option_list + (
580+        make_option('--first', action='store_false', dest='all', default=True,
581+                    help="Only return the first match for each static file."),
582+    )
583+
584+    def handle_label(self, media_path, **options):
585+        logger = self.get_logger()
586+        all = options['all']
587+        result = finders.find(media_path, all=all)
588+        if not result:
589+            logger.warning("No matching file found for %r." % media_path)
590+            return
591+        output = '\n  '.join((os.path.realpath(path) for path in result))
592+        logger.info("Found %r here:\n  %s" % (media_path, output))
593diff --git a/django/contrib/staticfiles/models.py b/django/contrib/staticfiles/models.py
594new file mode 100644
595index 0000000..e69de29
596diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py
597new file mode 100644
598index 0000000..b8d883c
599--- /dev/null
600+++ b/django/contrib/staticfiles/storage.py
601@@ -0,0 +1,29 @@
602+import os, posixpath
603+
604+from django.core.exceptions import ImproperlyConfigured
605+from django.conf import settings
606+from django.core.files.storage import FileSystemStorage, get_storage_class
607+from django.utils.functional import LazyObject
608+
609+
610+class StaticFilesStorage(FileSystemStorage):
611+    """
612+    Standard file system storage for site media files.
613+   
614+    The defaults for ``location`` and ``base_url`` are
615+    ``STATICFILES_ROOT`` and ``STATICFILES_URL``.
616+    """
617+    def __init__(self, location=None, base_url=None, *args, **kwargs):
618+        if location is None:
619+            location = settings.STATICFILES_ROOT
620+        if base_url is None:
621+            base_url = settings.STATICFILES_URL
622+        if not location:
623+            raise ImproperlyConfigured("You're using the staticfiles app "
624+                "without having set the STATICFILES_ROOT setting. Set it to "
625+                "the absolute path of the directory that holds static media.")
626+        if not base_url:
627+            raise ImproperlyConfigured("You're using the staticfiles app "
628+                "without having set the STATICFILES_URL setting. Set it to "
629+                "URL that handles the files served from STATICFILES_ROOT.")
630+        super(StaticFilesStorage, self).__init__(location, base_url, *args, **kwargs)
631diff --git a/django/contrib/staticfiles/urls.py b/django/contrib/staticfiles/urls.py
632new file mode 100644
633index 0000000..f8a30ac
634--- /dev/null
635+++ b/django/contrib/staticfiles/urls.py
636@@ -0,0 +1,10 @@
637+from django.conf.urls.defaults import patterns, url
638+from django.conf import settings
639+
640+urlpatterns = []
641+
642+# only serve non-fqdn URLs
643+if ':' not in settings.STATICFILES_URL:
644+    urlpatterns += patterns('',
645+        url(r'^(?P<path>.*)$', 'django.contrib.staticfiles.views.serve'),
646+    )
647diff --git a/django/contrib/staticfiles/utils.py b/django/contrib/staticfiles/utils.py
648new file mode 100644
649index 0000000..b533747
650--- /dev/null
651+++ b/django/contrib/staticfiles/utils.py
652@@ -0,0 +1,81 @@
653+import os
654+import fnmatch
655+
656+from django.core.files.storage import FileSystemStorage
657+from django.utils.importlib import import_module
658+
659+def get_files_for_app(app, ignore_patterns=[]):
660+    """
661+    Return a list containing the relative source paths for all files that
662+    should be copied for an app.
663+   
664+    """
665+    prefix = get_app_prefix(app)
666+    files = []
667+    storage = app_static_storage(app)
668+    if storage:
669+        for path in get_files(storage, ignore_patterns):
670+            if prefix:
671+                path = '/'.join([prefix, path])
672+            files.append(path)
673+    return files
674+
675+def app_static_storage(app):
676+    """
677+    Returns a static file storage if available in the given app.
678+   
679+    """
680+    # "app" is actually the models module of the app. Remove the '.models'.
681+    app_module = '.'.join(app.__name__.split('.')[:-1])
682+
683+    # The models module may be a package in which case dirname(app.__file__)
684+    # would be wrong. Import the actual app as opposed to the models module.
685+    app = import_module(app_module)
686+    app_root = os.path.dirname(app.__file__)
687+    location = os.path.join(app_root, 'media')
688+    if not os.path.isdir(location):
689+        return None
690+    return FileSystemStorage(location=location)
691+
692+def get_app_prefix(app):
693+    """
694+    Return the path name that should be prepended to files for this app.
695+   
696+    """
697+    # "app" is actually the models module of the app. Remove the '.models'.
698+    bits = app.__name__.split('.')[:-1]
699+    app_name = bits[-1]
700+    app_module = '.'.join(bits)
701+    if app_module == 'django.contrib.admin':
702+        return app_name
703+    else:
704+        return None
705+
706+def get_files(storage, ignore_patterns=[], location=''):
707+    """
708+    Recursively walk the storage directories gathering a complete list of files
709+    that should be copied, returning this list.
710+   
711+    """
712+    def is_ignored(path):
713+        """
714+        Return True or False depending on whether the ``path`` should be
715+        ignored (if it matches any pattern in ``ignore_patterns``).
716+       
717+        """
718+        for pattern in ignore_patterns:
719+            if fnmatch.fnmatchcase(path, pattern):
720+                return True
721+        return False
722+
723+    directories, files = storage.listdir(location)
724+    static_files = [location and '/'.join([location, fn]) or fn
725+                    for fn in files
726+                    if not is_ignored(fn)]
727+    for dir in directories:
728+        if is_ignored(dir):
729+            continue
730+        if location:
731+            dir = '/'.join([location, dir])
732+        static_files.extend(get_files(storage, ignore_patterns, dir))
733+    return static_files
734diff --git a/django/contrib/staticfiles/views.py b/django/contrib/staticfiles/views.py
735new file mode 100644
736index 0000000..ffd5264
737--- /dev/null
738+++ b/django/contrib/staticfiles/views.py
739@@ -0,0 +1,31 @@
740+"""
741+Views and functions for serving static files. These are only to be used during
742+development, and SHOULD NOT be used in a production setting.
743+
744+"""
745+import os
746+from django import http
747+from django.views import static
748+
749+from django.contrib.staticfiles import finders
750+
751+
752+def serve(request, path, show_indexes=False):
753+    """
754+    Serve static files from locations inferred from the static files finders.
755+
756+    To use, put a URL pattern such as::
757+
758+        (r'^(?P<path>.*)$', 'django.contrib.staticfiles.views.serve')
759+
760+    in your URLconf. You may also set ``show_indexes`` to ``True`` if you'd
761+    like to serve a basic index of the directory.  This index view will use the
762+    template hardcoded below, but if you'd like to override it, you can create
763+    a template called ``static/directory_index``.
764+    """
765+    absolute_path = finders.find(path)
766+    if not absolute_path:
767+        raise http.Http404('%r could not be matched to a static file.' % path)
768+    absolute_path, filename = os.path.split(absolute_path)
769+    return static.serve(request, path=filename, document_root=absolute_path,
770+                        show_indexes=show_indexes)
771diff --git a/django/core/context_processors.py b/django/core/context_processors.py
772index a5f29df..08bdc75 100644
773--- a/django/core/context_processors.py
774+++ b/django/core/context_processors.py
775@@ -71,7 +71,15 @@ def media(request):
776     Adds media-related context variables to the context.
777 
778     """
779-    return {'MEDIA_URL': settings.MEDIA_URL}
780+    import warnings
781+    warnings.warn(
782+        "The context processor at `django.core.context_processors.media` is " \
783+        "deprecated; use the path `django.contrib.staticfiles.context_processors.media` " \
784+        "instead.",
785+        PendingDeprecationWarning
786+    )
787+    from django.contrib.staticfiles.context_processors import media as media_context_processor
788+    return media_context_processor(request)
789 
790 def request(request):
791     return {'request': request}
792diff --git a/django/core/management/base.py b/django/core/management/base.py
793index dcd83ff..e00ebc1 100644
794--- a/django/core/management/base.py
795+++ b/django/core/management/base.py
796@@ -5,6 +5,7 @@ be executed through ``django-admin.py`` or ``manage.py``).
797 """
798 
799 import os
800+import logging
801 import sys
802 from optparse import make_option, OptionParser
803 
804@@ -135,9 +136,25 @@ class BaseCommand(object):
805     requires_model_validation = True
806     output_transaction = False # Whether to wrap the output in a "BEGIN; COMMIT;"
807 
808+    # OMG FIXME -- what is wrong with you? we don't want to use our own logger here, use the real thing man!
809+    logger_parent = 'django.core.management.base.commands'
810+    logger_name = 'base'
811+    logger_handler = logging.StreamHandler()
812+    logger_format = '%(message)s'
813+    logger_levels = {
814+        0: logging.ERROR,
815+        1: logging.INFO,
816+        2: logging.DEBUG,
817+    }
818+
819     def __init__(self):
820         self.style = color_style()
821 
822+    def get_logger(self):
823+        parts = [self.logger_parent, self.logger_name]
824+        name = '.'.join([part for part in parts if part])
825+        return logging.getLogger(name)
826+
827     def get_version(self):
828         """
829         Return the Django version, which should be correct for all
830@@ -199,6 +216,12 @@ class BaseCommand(object):
831         stderr.
832 
833         """
834+        verbosity = options.get('verbosity', 1)
835+        logger = self.get_logger()
836+        logger.setLevel(self.logger_levels[int(verbosity)])
837+        formatter = logging.Formatter(self.logger_format)
838+        self.logger_handler.setFormatter(formatter)
839+        logger.addHandler(self.logger_handler)
840         # Switch to English, because django-admin.py creates database content
841         # like permissions, and those shouldn't contain any translations.
842         # But only do this if we can assume we have a working settings file,
843@@ -232,6 +255,8 @@ class BaseCommand(object):
844         except CommandError, e:
845             self.stderr.write(smart_str(self.style.ERROR('Error: %s\n' % e)))
846             sys.exit(1)
847+        finally:
848+            logger.removeHandler(self.logger_handler)
849 
850     def validate(self, app=None, display_num_errors=False):
851         """
852@@ -297,6 +322,101 @@ class AppCommand(BaseCommand):
853         """
854         raise NotImplementedError()
855 
856+class OptionalAppCommand(BaseCommand):
857+    """
858+    A management command which optionally takes one or more installed
859+    application names as arguments, and does something with each of them.
860+
861+    If no application names are provided, all the applications are used.
862+
863+    The order in which applications are processed is determined by the order
864+    given in INSTALLED_APPS. This differs from Django's AppCommand (it uses the
865+    order the apps are given in the management command).
866+
867+    Rather than implementing ``handle()``, subclasses must implement
868+    ``handle_app()``, which will be called once for each application.
869+
870+    Subclasses can also optionally implement ``excluded_app()`` to run
871+    processes on apps which were excluded.
872+
873+    """
874+    args = '[appname appname ...]'
875+    option_list = BaseCommand.option_list + (
876+        make_option('-e', '--exclude', dest='exclude', action='append',
877+            default=[], help='App to exclude (use multiple --exclude to '
878+            'exclude multiple apps).'),
879+    )
880+
881+    def handle(self, *app_labels, **options):
882+        from django.db import models
883+        # Get all the apps, checking for common errors.
884+        try:
885+            all_apps = models.get_apps()
886+        except (ImproperlyConfigured, ImportError), e:
887+            raise CommandError("%s. Are you sure your INSTALLED_APPS setting "
888+                               "is correct?" % e)
889+        # Build the app_list.
890+        app_list = []
891+        used = 0
892+        for app in all_apps:
893+            app_label = app.__name__.split('.')[-2]
894+            if not app_labels or app_label in app_labels:
895+                used += 1
896+                if app_label not in options['exclude']:
897+                    app_list.append(app)
898+        # Check that all app_labels were used.
899+        if app_labels and used != len(app_labels):
900+            raise CommandError('Could not find the following app(s): %s' %
901+                               ', '.join(app_labels))
902+        # Handle all the apps (either via handle_app or excluded_app),
903+        # collating any output.
904+        output = []
905+        pre_output = self.pre_handle_apps(**options)
906+        if pre_output:
907+            output.append(pre_output)
908+        for app in all_apps:
909+            if app in app_list:
910+                handle_method = self.handle_app
911+            else:
912+                handle_method = self.excluded_app
913+            app_output = handle_method(app, **options)
914+            if app_output:
915+                output.append(app_output)
916+        post_output = self.post_handle_apps(**options)
917+        if post_output:
918+            output.append(post_output)
919+        return '\n'.join(output)
920+
921+    def handle_app(self, app, **options):
922+        """
923+        Perform the command's actions for ``app``, which will be the
924+        Python module corresponding to an application name given on
925+        the command line.
926+
927+        """
928+        raise NotImplementedError()
929+
930+    def excluded_app(self, app, **options):
931+        """
932+        A hook for commands to parse apps which were excluded.
933+
934+        """
935+
936+    def pre_handle_apps(self, **options):
937+        """
938+        A hook for commands to do something before the applications are
939+        processed.
940+
941+        """
942+
943+    def post_handle_apps(self, **options):
944+        """
945+        A hook for commands to do something after all applications have been
946+        processed.
947+
948+        """
949+
950+
951 class LabelCommand(BaseCommand):
952     """
953     A management command which takes one or more arbitrary arguments
954diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py
955index fc2c694..21391e8 100644
956--- a/django/core/management/commands/runserver.py
957+++ b/django/core/management/commands/runserver.py
958@@ -1,7 +1,9 @@
959-from django.core.management.base import BaseCommand, CommandError
960 from optparse import make_option
961 import os
962 import sys
963+import warnings
964+
965+from django.core.management.base import BaseCommand, CommandError
966 
967 class Command(BaseCommand):
968     option_list = BaseCommand.option_list + (
969@@ -20,6 +22,7 @@ class Command(BaseCommand):
970         import django
971         from django.core.servers.basehttp import run, AdminMediaHandler, WSGIServerException
972         from django.core.handlers.wsgi import WSGIHandler
973+        from django.contrib.staticfiles.handlers import StaticFilesHandler
974         if args:
975             raise CommandError('Usage is runserver %s' % self.args)
976         if not addrport:
977@@ -56,7 +59,10 @@ class Command(BaseCommand):
978             translation.activate(settings.LANGUAGE_CODE)
979 
980             try:
981-                handler = AdminMediaHandler(WSGIHandler(), admin_media_path)
982+                handler = WSGIHandler()
983+                handler = StaticFilesHandler(handler)
984+                # serve admin media like old-school (deprecation pending)
985+                handler = AdminMediaHandler(handler, admin_media_path)
986                 run(addr, int(port), handler)
987             except WSGIServerException, e:
988                 # Use helpful error messages instead of ugly tracebacks.
989diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py
990index dae4297..2da05de 100644
991--- a/django/core/servers/basehttp.py
992+++ b/django/core/servers/basehttp.py
993@@ -8,16 +8,17 @@ been reviewed for security issues. Don't use it for production use.
994 """
995 
996 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
997-import mimetypes
998 import os
999 import re
1000-import stat
1001 import sys
1002 import urllib
1003+import warnings
1004 
1005 from django.core.management.color import color_style
1006 from django.utils.http import http_date
1007 from django.utils._os import safe_join
1008+from django.contrib.staticfiles.handlers import StaticFilesHandler
1009+from django.views import static
1010 
1011 __version__ = "0.1"
1012 __all__ = ['WSGIServer','WSGIRequestHandler']
1013@@ -633,86 +634,48 @@ class WSGIRequestHandler(BaseHTTPRequestHandler):
1014 
1015         sys.stderr.write(msg)
1016 
1017-class AdminMediaHandler(object):
1018+
1019+class AdminMediaHandler(StaticFilesHandler):
1020     """
1021     WSGI middleware that intercepts calls to the admin media directory, as
1022     defined by the ADMIN_MEDIA_PREFIX setting, and serves those images.
1023     Use this ONLY LOCALLY, for development! This hasn't been tested for
1024     security and is not super efficient.
1025     """
1026-    def __init__(self, application, media_dir=None):
1027+
1028+    @property
1029+    def media_dir(self):
1030+        import django
1031+        return os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
1032+
1033+    @property
1034+    def media_url(self):
1035         from django.conf import settings
1036-        self.application = application
1037-        if not media_dir:
1038-            import django
1039-            self.media_dir = \
1040-                os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
1041-        else:
1042-            self.media_dir = media_dir
1043-        self.media_url = settings.ADMIN_MEDIA_PREFIX
1044+        return settings.ADMIN_MEDIA_PREFIX
1045+
1046+    def __init__(self, application, media_dir=None):
1047+        warnings.warn('The AdminMediaHandler handler is deprecated; use the '
1048+            '`django.contrib.staticfiles.handlers.StaticFilesHandler` instead.',
1049+            PendingDeprecationWarning)
1050+        super(AdminMediaHandler, self).__init__(application, media_dir)
1051 
1052     def file_path(self, url):
1053         """
1054         Returns the path to the media file on disk for the given URL.
1055 
1056-        The passed URL is assumed to begin with ADMIN_MEDIA_PREFIX.  If the
1057+        The passed URL is assumed to begin with ``media_url``.  If the
1058         resultant file path is outside the media directory, then a ValueError
1059         is raised.
1060         """
1061-        # Remove ADMIN_MEDIA_PREFIX.
1062+        # Remove ``media_url``.
1063         relative_url = url[len(self.media_url):]
1064         relative_path = urllib.url2pathname(relative_url)
1065         return safe_join(self.media_dir, relative_path)
1066 
1067-    def __call__(self, environ, start_response):
1068-        import os.path
1069-
1070-        # Ignore requests that aren't under ADMIN_MEDIA_PREFIX. Also ignore
1071-        # all requests if ADMIN_MEDIA_PREFIX isn't a relative URL.
1072-        if self.media_url.startswith('http://') or self.media_url.startswith('https://') \
1073-            or not environ['PATH_INFO'].startswith(self.media_url):
1074-            return self.application(environ, start_response)
1075+    def serve(self, request, path):
1076+        document_root, path = os.path.split(path)
1077+        return static.serve(request, path, document_root=document_root)
1078 
1079-        # Find the admin file and serve it up, if it exists and is readable.
1080-        try:
1081-            file_path = self.file_path(environ['PATH_INFO'])
1082-        except ValueError: # Resulting file path was not valid.
1083-            status = '404 NOT FOUND'
1084-            headers = {'Content-type': 'text/plain'}
1085-            output = ['Page not found: %s' % environ['PATH_INFO']]
1086-            start_response(status, headers.items())
1087-            return output
1088-        if not os.path.exists(file_path):
1089-            status = '404 NOT FOUND'
1090-            headers = {'Content-type': 'text/plain'}
1091-            output = ['Page not found: %s' % environ['PATH_INFO']]
1092-        else:
1093-            try:
1094-                fp = open(file_path, 'rb')
1095-            except IOError:
1096-                status = '401 UNAUTHORIZED'
1097-                headers = {'Content-type': 'text/plain'}
1098-                output = ['Permission denied: %s' % environ['PATH_INFO']]
1099-            else:
1100-                # This is a very simple implementation of conditional GET with
1101-                # the Last-Modified header. It makes media files a bit speedier
1102-                # because the files are only read off disk for the first
1103-                # request (assuming the browser/client supports conditional
1104-                # GET).
1105-                mtime = http_date(os.stat(file_path)[stat.ST_MTIME])
1106-                headers = {'Last-Modified': mtime}
1107-                if environ.get('HTTP_IF_MODIFIED_SINCE', None) == mtime:
1108-                    status = '304 NOT MODIFIED'
1109-                    output = []
1110-                else:
1111-                    status = '200 OK'
1112-                    mime_type = mimetypes.guess_type(file_path)[0]
1113-                    if mime_type:
1114-                        headers['Content-Type'] = mime_type
1115-                    output = [fp.read()]
1116-                    fp.close()
1117-        start_response(status, headers.items())
1118-        return output
1119 
1120 def run(addr, port, wsgi_handler):
1121     server_address = (addr, port)
1122diff --git a/tests/regressiontests/servers/tests.py b/tests/regressiontests/servers/tests.py
1123index 4763982..29f169c 100644
1124--- a/tests/regressiontests/servers/tests.py
1125+++ b/tests/regressiontests/servers/tests.py
1126@@ -9,6 +9,7 @@ from django.test import TestCase
1127 from django.core.handlers.wsgi import WSGIHandler
1128 from django.core.servers.basehttp import AdminMediaHandler
1129 
1130+from django.conf import settings
1131 
1132 class AdminMediaHandlerTests(TestCase):
1133 
1134@@ -25,7 +26,7 @@ class AdminMediaHandlerTests(TestCase):
1135         """
1136         # Cases that should work on all platforms.
1137         data = (
1138-            ('/media/css/base.css', ('css', 'base.css')),
1139+            ('%scss/base.css' % settings.ADMIN_MEDIA_PREFIX, ('css', 'base.css')),
1140         )
1141         # Cases that should raise an exception.
1142         bad_data = ()
1143@@ -34,19 +35,19 @@ class AdminMediaHandlerTests(TestCase):
1144         if os.sep == '/':
1145             data += (
1146                 # URL, tuple of relative path parts.
1147-                ('/media/\\css/base.css', ('\\css', 'base.css')),
1148+                ('%s\\css/base.css' % settings.ADMIN_MEDIA_PREFIX, ('\\css', 'base.css')),
1149             )
1150             bad_data += (
1151-                '/media//css/base.css',
1152-                '/media////css/base.css',
1153-                '/media/../css/base.css',
1154+                '%s/css/base.css' % settings.ADMIN_MEDIA_PREFIX,
1155+                '%s///css/base.css' % settings.ADMIN_MEDIA_PREFIX,
1156+                '%s../css/base.css' % settings.ADMIN_MEDIA_PREFIX,
1157             )
1158         elif os.sep == '\\':
1159             bad_data += (
1160-                '/media/C:\css/base.css',
1161-                '/media//\\css/base.css',
1162-                '/media/\\css/base.css',
1163-                '/media/\\\\css/base.css'
1164+                '%sC:\css/base.css' % settings.ADMIN_MEDIA_PREFIX,
1165+                '%s/\\css/base.css' % settings.ADMIN_MEDIA_PREFIX,
1166+                '%s\\css/base.css' % settings.ADMIN_MEDIA_PREFIX,
1167+                '%s\\\\css/base.css' % settings.ADMIN_MEDIA_PREFIX
1168             )
1169         for url, path_tuple in data:
1170             try:
1171diff --git a/tests/regressiontests/staticfiles_tests/__init__.py b/tests/regressiontests/staticfiles_tests/__init__.py
1172new file mode 100644
1173index 0000000..e69de29
1174diff --git a/tests/regressiontests/staticfiles_tests/apps/__init__.py b/tests/regressiontests/staticfiles_tests/apps/__init__.py
1175new file mode 100644
1176index 0000000..e69de29
1177diff --git a/tests/regressiontests/staticfiles_tests/apps/no_label/__init__.py b/tests/regressiontests/staticfiles_tests/apps/no_label/__init__.py
1178new file mode 100644
1179index 0000000..e69de29
1180diff --git a/tests/regressiontests/staticfiles_tests/apps/no_label/media/file2.txt b/tests/regressiontests/staticfiles_tests/apps/no_label/media/file2.txt
1181new file mode 100644
1182index 0000000..aa264ca
1183--- /dev/null
1184+++ b/tests/regressiontests/staticfiles_tests/apps/no_label/media/file2.txt
1185@@ -0,0 +1 @@
1186+file2 in no_label_app
1187diff --git a/tests/regressiontests/staticfiles_tests/apps/no_label/models.py b/tests/regressiontests/staticfiles_tests/apps/no_label/models.py
1188new file mode 100644
1189index 0000000..e69de29
1190diff --git a/tests/regressiontests/staticfiles_tests/apps/test/__init__.py b/tests/regressiontests/staticfiles_tests/apps/test/__init__.py
1191new file mode 100644
1192index 0000000..e69de29
1193diff --git a/tests/regressiontests/staticfiles_tests/apps/test/media/test/.hidden b/tests/regressiontests/staticfiles_tests/apps/test/media/test/.hidden
1194new file mode 100644
1195index 0000000..cef6c23
1196--- /dev/null
1197+++ b/tests/regressiontests/staticfiles_tests/apps/test/media/test/.hidden
1198@@ -0,0 +1 @@
1199+This file should be ignored.
1200diff --git a/tests/regressiontests/staticfiles_tests/apps/test/media/test/CVS b/tests/regressiontests/staticfiles_tests/apps/test/media/test/CVS
1201new file mode 100644
1202index 0000000..cef6c23
1203--- /dev/null
1204+++ b/tests/regressiontests/staticfiles_tests/apps/test/media/test/CVS
1205@@ -0,0 +1 @@
1206+This file should be ignored.
1207diff --git a/tests/regressiontests/staticfiles_tests/apps/test/media/test/backup~ b/tests/regressiontests/staticfiles_tests/apps/test/media/test/backup~
1208new file mode 100644
1209index 0000000..cef6c23
1210--- /dev/null
1211+++ b/tests/regressiontests/staticfiles_tests/apps/test/media/test/backup~
1212@@ -0,0 +1 @@
1213+This file should be ignored.
1214diff --git a/tests/regressiontests/staticfiles_tests/apps/test/media/test/file.txt b/tests/regressiontests/staticfiles_tests/apps/test/media/test/file.txt
1215new file mode 100644
1216index 0000000..169a206
1217--- /dev/null
1218+++ b/tests/regressiontests/staticfiles_tests/apps/test/media/test/file.txt
1219@@ -0,0 +1 @@
1220+In app media directory.
1221diff --git a/tests/regressiontests/staticfiles_tests/apps/test/media/test/file1.txt b/tests/regressiontests/staticfiles_tests/apps/test/media/test/file1.txt
1222new file mode 100644
1223index 0000000..9f9a8d9
1224--- /dev/null
1225+++ b/tests/regressiontests/staticfiles_tests/apps/test/media/test/file1.txt
1226@@ -0,0 +1 @@
1227+file1 in the app dir
1228\ No newline at end of file
1229diff --git a/tests/regressiontests/staticfiles_tests/apps/test/media/test/test.ignoreme b/tests/regressiontests/staticfiles_tests/apps/test/media/test/test.ignoreme
1230new file mode 100644
1231index 0000000..d7df09c
1232--- /dev/null
1233+++ b/tests/regressiontests/staticfiles_tests/apps/test/media/test/test.ignoreme
1234@@ -0,0 +1 @@
1235+This file should be ignored.
1236\ No newline at end of file
1237diff --git a/tests/regressiontests/staticfiles_tests/apps/test/models.py b/tests/regressiontests/staticfiles_tests/apps/test/models.py
1238new file mode 100644
1239index 0000000..e69de29
1240diff --git a/tests/regressiontests/staticfiles_tests/apps/test/otherdir/odfile.txt b/tests/regressiontests/staticfiles_tests/apps/test/otherdir/odfile.txt
1241new file mode 100644
1242index 0000000..c62c93d
1243--- /dev/null
1244+++ b/tests/regressiontests/staticfiles_tests/apps/test/otherdir/odfile.txt
1245@@ -0,0 +1 @@
1246+File in otherdir.
1247diff --git a/tests/regressiontests/staticfiles_tests/models.py b/tests/regressiontests/staticfiles_tests/models.py
1248new file mode 100644
1249index 0000000..e69de29
1250diff --git a/tests/regressiontests/staticfiles_tests/project/documents/subdir/test.txt b/tests/regressiontests/staticfiles_tests/project/documents/subdir/test.txt
1251new file mode 100644
1252index 0000000..04326a2
1253--- /dev/null
1254+++ b/tests/regressiontests/staticfiles_tests/project/documents/subdir/test.txt
1255@@ -0,0 +1 @@
1256+Can we find this file?
1257diff --git a/tests/regressiontests/staticfiles_tests/project/documents/test.txt b/tests/regressiontests/staticfiles_tests/project/documents/test.txt
1258new file mode 100644
1259index 0000000..04326a2
1260--- /dev/null
1261+++ b/tests/regressiontests/staticfiles_tests/project/documents/test.txt
1262@@ -0,0 +1 @@
1263+Can we find this file?
1264diff --git a/tests/regressiontests/staticfiles_tests/project/documents/test/file.txt b/tests/regressiontests/staticfiles_tests/project/documents/test/file.txt
1265new file mode 100644
1266index 0000000..fdeaa23
1267--- /dev/null
1268+++ b/tests/regressiontests/staticfiles_tests/project/documents/test/file.txt
1269@@ -0,0 +1,2 @@
1270+In STATICFILES_DIRS directory.
1271+
1272diff --git a/tests/regressiontests/staticfiles_tests/project/site_media/media/media-file.txt b/tests/regressiontests/staticfiles_tests/project/site_media/media/media-file.txt
1273new file mode 100644
1274index 0000000..466922d
1275--- /dev/null
1276+++ b/tests/regressiontests/staticfiles_tests/project/site_media/media/media-file.txt
1277@@ -0,0 +1 @@
1278+Media file.
1279diff --git a/tests/regressiontests/staticfiles_tests/project/site_media/static/test/storage.txt b/tests/regressiontests/staticfiles_tests/project/site_media/static/test/storage.txt
1280new file mode 100644
1281index 0000000..2eda9ce
1282--- /dev/null
1283+++ b/tests/regressiontests/staticfiles_tests/project/site_media/static/test/storage.txt
1284@@ -0,0 +1 @@
1285+Yeah!
1286\ No newline at end of file
1287diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py
1288new file mode 100644
1289index 0000000..0f29d60
1290--- /dev/null
1291+++ b/tests/regressiontests/staticfiles_tests/tests.py
1292@@ -0,0 +1,304 @@
1293+import tempfile
1294+import shutil
1295+import os
1296+import sys
1297+from cStringIO import StringIO
1298+import posixpath
1299+
1300+from django.test import TestCase, Client
1301+from django.conf import settings
1302+from django.core.exceptions import ImproperlyConfigured
1303+from django.core.management import call_command
1304+from django.db.models.loading import load_app
1305+
1306+from django.contrib.staticfiles import finders, storage
1307+
1308+TEST_ROOT = os.path.dirname(__file__)
1309+
1310+
1311+class StaticFilesTestCase(TestCase):
1312+    """
1313+    Test case with a couple utility assertions.
1314+    """
1315+    def setUp(self):
1316+        self.old_staticfiles_url = settings.STATICFILES_URL
1317+        self.old_staticfiles_root = settings.STATICFILES_ROOT
1318+        self.old_staticfiles_dirs = settings.STATICFILES_DIRS
1319+        self.old_installed_apps = settings.INSTALLED_APPS
1320+        self.old_media_root = settings.MEDIA_ROOT
1321+        self.old_media_url = settings.MEDIA_URL
1322+        self.old_admin_media_prefix = settings.ADMIN_MEDIA_PREFIX
1323+
1324+        # We have to load these apps to test staticfiles.
1325+        load_app('regressiontests.staticfiles_tests.apps.test')
1326+        load_app('regressiontests.staticfiles_tests.apps.no_label')
1327+        site_media = os.path.join(TEST_ROOT, 'project', 'site_media')
1328+        settings.MEDIA_ROOT =  os.path.join(site_media, 'media')
1329+        settings.MEDIA_URL = '/media/'
1330+        settings.ADMIN_MEDIA_PREFIX = posixpath.join(settings.STATICFILES_URL, 'admin/')
1331+        settings.STATICFILES_ROOT = os.path.join(site_media, 'static')
1332+        settings.STATICFILES_URL = '/static/'
1333+        settings.STATICFILES_DIRS = (
1334+            os.path.join(TEST_ROOT, 'project', 'documents'),
1335+        )
1336+
1337+    def tearDown(self):
1338+        settings.MEDIA_ROOT = self.old_media_root
1339+        settings.MEDIA_URL = self.old_media_url
1340+        settings.ADMIN_MEDIA_PREFIX = self.old_admin_media_prefix
1341+        settings.STATICFILES_ROOT = self.old_staticfiles_root
1342+        settings.STATICFILES_URL = self.old_staticfiles_url
1343+        settings.STATICFILES_DIRS = self.old_staticfiles_dirs
1344+        settings.INSTALLED_APPS = self.old_installed_apps
1345+
1346+    def assertFileContains(self, filepath, text):
1347+        self.failUnless(text in self._get_file(filepath),
1348+                        "'%s' not in '%s'" % (text, filepath))
1349+
1350+    def assertFileNotFound(self, filepath):
1351+        self.assertRaises(IOError, self._get_file, filepath)
1352+
1353+
1354+class TestingStaticFilesStorage(storage.StaticFilesStorage):
1355+
1356+    def __init__(self, location=None, base_url=None, *args, **kwargs):
1357+        location = tempfile.mkdtemp()
1358+        settings.STATICFILES_ROOT = location
1359+        super(TestingStaticFilesStorage, self).__init__(location, base_url, *args, **kwargs)
1360+
1361+
1362+class BuildStaticTestCase(StaticFilesTestCase):
1363+    """
1364+    Tests shared by all file-resolving features (build_static,
1365+    find_static, and static serve view).
1366+
1367+    This relies on the asserts defined in UtilityAssertsTestCase, but
1368+    is separated because some test cases need those asserts without
1369+    all these tests.
1370+    """
1371+    def setUp(self):
1372+        super(BuildStaticTestCase, self).setUp()
1373+        self.old_staticfiles_storage = settings.STATICFILES_STORAGE
1374+        self.old_root = settings.STATICFILES_ROOT
1375+        settings.STATICFILES_STORAGE = 'regressiontests.staticfiles_tests.tests.TestingStaticFilesStorage'
1376+        self.run_build_static()
1377+
1378+    def tearDown(self):
1379+        shutil.rmtree(settings.STATICFILES_ROOT)
1380+        settings.STATICFILES_ROOT = self.old_root
1381+        settings.STATICFILES_STORAGE = self.old_staticfiles_storage
1382+        super(BuildStaticTestCase, self).tearDown()
1383+
1384+    def run_build_static(self, **kwargs):
1385+        call_command('build_static', interactive=False, verbosity='0',
1386+                     ignore_patterns=['*.ignoreme'], **kwargs)
1387+
1388+    def _get_file(self, filepath):
1389+        assert filepath, 'filepath is empty.'
1390+        filepath = os.path.join(settings.STATICFILES_ROOT, filepath)
1391+        return open(filepath).read()
1392+
1393+
1394+class TestDefaults(object):
1395+    def test_staticfiles_dirs(self):
1396+        """
1397+        Can find a file in a STATICFILES_DIRS directory.
1398+
1399+        """
1400+        self.assertFileContains('test.txt', 'Can we find')
1401+
1402+    def test_staticfiles_dirs_subdir(self):
1403+        """
1404+        Can find a file in a subdirectory of a STATICFILES_DIRS
1405+        directory.
1406+
1407+        """
1408+        self.assertFileContains('subdir/test.txt', 'Can we find')
1409+
1410+    def test_staticfiles_dirs_priority(self):
1411+        """
1412+        File in STATICFILES_DIRS has priority over file in app.
1413+
1414+        """
1415+        self.assertFileContains('test/file.txt', 'STATICFILES_DIRS')
1416+
1417+    def test_app_files(self):
1418+        """
1419+        Can find a file in an app media/ directory.
1420+
1421+        """
1422+        self.assertFileContains('test/file1.txt', 'file1 in the app dir')
1423+
1424+
1425+class TestBuildStatic(BuildStaticTestCase, TestDefaults):
1426+    """
1427+    Test ``build_static`` management command.
1428+    """
1429+    def test_ignore(self):
1430+        """
1431+        Test that -i patterns are ignored.
1432+        """
1433+        self.assertFileNotFound('test/test.ignoreme')
1434+
1435+    def test_common_ignore_patterns(self):
1436+        """
1437+        Common ignore patterns (*~, .*, CVS) are ignored.
1438+        """
1439+        self.assertFileNotFound('test/.hidden')
1440+        self.assertFileNotFound('test/backup~')
1441+        self.assertFileNotFound('test/CVS')
1442+
1443+
1444+class TestBuildStaticExcludeNoDefaultIgnore(BuildStaticTestCase):
1445+    """
1446+    Test ``--exclude-dirs`` and ``--no-default-ignore`` options for
1447+    ``build_static`` management command.
1448+    """
1449+    def run_build_static(self):
1450+        super(TestBuildStaticExcludeNoDefaultIgnore, self).run_build_static(
1451+            exclude_dirs=True, use_default_ignore_patterns=False)
1452+
1453+    def test_exclude_dirs(self):
1454+        """
1455+        With --exclude-dirs, cannot find file in
1456+        STATICFILES_DIRS.
1457+
1458+        """
1459+        self.assertFileNotFound('test.txt')
1460+
1461+    def test_no_common_ignore_patterns(self):
1462+        """
1463+        With --no-default-ignore, common ignore patterns (*~, .*, CVS)
1464+        are not ignored.
1465+
1466+        """
1467+        self.assertFileContains('test/.hidden', 'should be ignored')
1468+        self.assertFileContains('test/backup~', 'should be ignored')
1469+        self.assertFileContains('test/CVS', 'should be ignored')
1470+
1471+
1472+class TestBuildStaticDryRun(BuildStaticTestCase):
1473+    """
1474+    Test ``--dry-run`` option for ``build_static`` management command.
1475+    """
1476+    def run_build_static(self):
1477+        super(TestBuildStaticDryRun, self).run_build_static(dry_run=True)
1478+
1479+    def test_no_files_created(self):
1480+        """
1481+        With --dry-run, no files created in destination dir.
1482+        """
1483+        self.assertEquals(os.listdir(settings.STATICFILES_ROOT), [])
1484+
1485+
1486+if sys.platform != 'win32':
1487+    class TestBuildStaticLinks(BuildStaticTestCase, TestDefaults):
1488+        """
1489+        Test ``--link`` option for ``build_static`` management command.
1490+
1491+        Note that by inheriting ``BaseFileResolutionTests`` we repeat all
1492+        the standard file resolving tests here, to make sure using
1493+        ``--link`` does not change the file-selection semantics.
1494+        """
1495+        def run_build_static(self):
1496+            super(TestBuildStaticLinks, self).run_build_static(link=True)
1497+
1498+        def test_links_created(self):
1499+            """
1500+            With ``--link``, symbolic links are created.
1501+
1502+            """
1503+            self.failUnless(os.path.islink(os.path.join(settings.STATICFILES_ROOT, 'test.txt')))
1504+
1505+
1506+class TestServeStatic(StaticFilesTestCase):
1507+    """
1508+    Test static asset serving view.
1509+    """
1510+    urls = "regressiontests.staticfiles_tests.urls"
1511+
1512+    def _response(self, filepath):
1513+        return self.client.get(
1514+            posixpath.join(settings.STATICFILES_URL, filepath))
1515+
1516+    def assertFileContains(self, filepath, text):
1517+        self.assertContains(self._response(filepath), text)
1518+
1519+    def assertFileNotFound(self, filepath):
1520+        self.assertEquals(self._response(filepath).status_code, 404)
1521+
1522+
1523+class TestServeAdminMedia(TestServeStatic):
1524+    """
1525+    Test serving media from django.contrib.admin.
1526+    """
1527+    def test_serve_admin_media(self):
1528+        media_file = posixpath.join(
1529+            settings.ADMIN_MEDIA_PREFIX, 'css/base.css')
1530+        response = self.client.get(media_file)
1531+        self.assertContains(response, 'body')
1532+
1533+
1534+class FinderTestCase(object):
1535+    """
1536+    Base finder test mixin
1537+    """
1538+    def test_find_first(self):
1539+        src, dst = self.find_first
1540+        self.assertEquals(self.finder.find(src), dst)
1541+
1542+    def test_find_all(self):
1543+        src, dst = self.find_all
1544+        self.assertEquals(self.finder.find(src, all=True), dst)
1545+
1546+
1547+class TestFileSystemFinder(StaticFilesTestCase, FinderTestCase):
1548+    """
1549+    Test FileSystemFinder.
1550+    """
1551+    def setUp(self):
1552+        super(TestFileSystemFinder, self).setUp()
1553+        self.finder = finders.FileSystemFinder()
1554+        test_file_path = os.path.join(TEST_ROOT, 'project/documents/test/file.txt')
1555+        self.find_first = ("test/file.txt", test_file_path)
1556+        self.find_all = ("test/file.txt", [test_file_path])
1557+
1558+
1559+class TestAppDirectoriesFinder(StaticFilesTestCase, FinderTestCase):
1560+    """
1561+    Test AppDirectoriesFinder.
1562+    """
1563+    def setUp(self):
1564+        super(TestAppDirectoriesFinder, self).setUp()
1565+        self.finder = finders.AppDirectoriesFinder()
1566+        test_file_path = os.path.join(TEST_ROOT, 'apps/test/media/test/file1.txt')
1567+        self.find_first = ("test/file1.txt", test_file_path)
1568+        self.find_all = ("test/file1.txt", [test_file_path])
1569+
1570+
1571+class TestStorageFinder(StaticFilesTestCase, FinderTestCase):
1572+    """
1573+    Test StorageFinder.
1574+    """
1575+    def setUp(self):
1576+        super(TestStorageFinder, self).setUp()
1577+        self.finder = finders.StorageFinder(
1578+            storage=storage.StaticFilesStorage(location=settings.MEDIA_ROOT))
1579+        test_file_path = os.path.join(settings.MEDIA_ROOT, 'media-file.txt')
1580+        self.find_first = ("media-file.txt", test_file_path)
1581+        self.find_all = ("media-file.txt", [test_file_path])
1582+
1583+
1584+class TestMiscFinder(TestCase):
1585+    """
1586+    A few misc finder tests.
1587+    """
1588+    def test_get_finder(self):
1589+        self.assertTrue(isinstance(finders.get_finder(
1590+            "django.contrib.staticfiles.finders.FileSystemFinder"),
1591+            finders.FileSystemFinder))
1592+        self.assertRaises(ImproperlyConfigured,
1593+            finders.get_finder, "django.contrib.staticfiles.finders.FooBarFinder")
1594+        self.assertRaises(ImproperlyConfigured,
1595+            finders.get_finder, "foo.bar.FooBarFinder")
1596+
1597diff --git a/tests/regressiontests/staticfiles_tests/urls.py b/tests/regressiontests/staticfiles_tests/urls.py
1598new file mode 100644
1599index 0000000..061ec64
1600--- /dev/null
1601+++ b/tests/regressiontests/staticfiles_tests/urls.py
1602@@ -0,0 +1,6 @@
1603+from django.conf import settings
1604+from django.conf.urls.defaults import *
1605+
1606+urlpatterns = patterns('',
1607+    url(r'^static/(?P<path>.*)$', 'django.contrib.staticfiles.views.serve'),
1608+)
1609diff --git a/tests/runtests.py b/tests/runtests.py
1610index a52152b..c23f9fd 100755
1611--- a/tests/runtests.py
1612+++ b/tests/runtests.py
1613@@ -28,6 +28,7 @@ ALWAYS_INSTALLED_APPS = [
1614     'django.contrib.comments',
1615     'django.contrib.admin',
1616     'django.contrib.admindocs',
1617+    'django.contrib.staticfiles',
1618 ]
1619 
1620 def get_test_models():
1621diff --git a/tests/urls.py b/tests/urls.py
1622index 01d6408..e06dc33 100644
1623--- a/tests/urls.py
1624+++ b/tests/urls.py
1625@@ -41,4 +41,7 @@ urlpatterns = patterns('',
1626 
1627     # special headers views
1628     (r'special_headers/', include('regressiontests.special_headers.urls')),
1629+
1630+    # static files handling
1631+    (r'^', include('regressiontests.staticfiles_tests.urls')),
1632 )