Code

Ticket #12323: 12323.6.diff

File 12323.6.diff, 60.6 KB (added by jezdez, 4 years ago)

Refactored utilities and management command.

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