Code

Ticket #12323: 12323.4.diff

File 12323.4.diff, 60.7 KB (added by jezdez, 4 years ago)

logging adoption and other smaller things

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