From cfc19f84def07fb950ae8789ed0655eae4f66a92 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 20 Oct 2010 01:33:24 +0000 Subject: [PATCH] Fixed #12323 and #11582 -- Extended the ability to handle static files. Thanks to all for helping with the original app, the patch, documentation and general support. git-svn-id: http://code.djangoproject.com/svn/django/trunk@14293 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 38 +- django/conf/project_template/settings.py | 30 +- django/contrib/staticfiles/__init__.py | 0 .../contrib/staticfiles/context_processors.py | 7 + django/contrib/staticfiles/finders.py | 254 ++++++++++ django/contrib/staticfiles/handlers.py | 72 +++ .../staticfiles/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/collectstatic.py | 184 +++++++ .../management/commands/findstatic.py | 24 + django/contrib/staticfiles/models.py | 0 django/contrib/staticfiles/storage.py | 84 ++++ .../staticfiles/templatetags/__init__.py | 0 .../staticfiles/templatetags/staticfiles.py | 43 ++ django/contrib/staticfiles/urls.py | 29 ++ django/contrib/staticfiles/utils.py | 30 ++ django/contrib/staticfiles/views.py | 159 ++++++ django/core/context_processors.py | 10 +- django/core/management/commands/runserver.py | 10 +- django/core/servers/basehttp.py | 85 +--- django/views/static.py | 117 +---- docs/howto/static-files.txt | 471 +++++++++++++----- docs/index.txt | 3 +- docs/ref/contrib/index.txt | 1 + docs/ref/contrib/staticfiles.txt | 283 +++++++++++ docs/ref/settings.txt | 2 +- docs/ref/templates/api.txt | 9 +- tests/regressiontests/servers/tests.py | 19 +- .../staticfiles_tests/__init__.py | 0 .../staticfiles_tests/apps/__init__.py | 0 .../apps/no_label/__init__.py | 0 .../staticfiles_tests/apps/no_label/models.py | 0 .../apps/no_label/static/file2.txt | 1 + .../staticfiles_tests/apps/test/__init__.py | 0 .../staticfiles_tests/apps/test/models.py | 0 .../apps/test/otherdir/odfile.txt | 1 + .../apps/test/static/test/.hidden | 1 + .../apps/test/static/test/CVS | 1 + .../apps/test/static/test/backup~ | 1 + .../apps/test/static/test/file.txt | 1 + .../apps/test/static/test/file1.txt | 1 + .../apps/test/static/test/test.ignoreme | 1 + .../staticfiles_tests/models.py | 0 .../project/documents/subdir/test.txt | 1 + .../project/documents/test.txt | 1 + .../project/documents/test/file.txt | 2 + .../project/site_media/media/media-file.txt | 1 + .../site_media/static/test/storage.txt | 1 + .../staticfiles_tests/tests.py | 330 ++++++++++++ .../staticfiles_tests/urls/__init__.py | 0 .../staticfiles_tests/urls/default.py | 6 + .../staticfiles_tests/urls/helper.py | 3 + tests/runtests.py | 1 + tests/urls.py | 3 + 54 files changed, 2008 insertions(+), 313 deletions(-) create mode 100644 django/contrib/staticfiles/__init__.py create mode 100644 django/contrib/staticfiles/context_processors.py create mode 100644 django/contrib/staticfiles/finders.py create mode 100644 django/contrib/staticfiles/handlers.py create mode 100644 django/contrib/staticfiles/management/__init__.py create mode 100644 django/contrib/staticfiles/management/commands/__init__.py create mode 100644 django/contrib/staticfiles/management/commands/collectstatic.py create mode 100644 django/contrib/staticfiles/management/commands/findstatic.py create mode 100644 django/contrib/staticfiles/models.py create mode 100644 django/contrib/staticfiles/storage.py create mode 100644 django/contrib/staticfiles/templatetags/__init__.py create mode 100644 django/contrib/staticfiles/templatetags/staticfiles.py create mode 100644 django/contrib/staticfiles/urls.py create mode 100644 django/contrib/staticfiles/utils.py create mode 100644 django/contrib/staticfiles/views.py create mode 100644 docs/ref/contrib/staticfiles.txt create mode 100644 tests/regressiontests/staticfiles_tests/__init__.py create mode 100644 tests/regressiontests/staticfiles_tests/apps/__init__.py create mode 100644 tests/regressiontests/staticfiles_tests/apps/no_label/__init__.py create mode 100644 tests/regressiontests/staticfiles_tests/apps/no_label/models.py create mode 100644 tests/regressiontests/staticfiles_tests/apps/no_label/static/file2.txt create mode 100644 tests/regressiontests/staticfiles_tests/apps/test/__init__.py create mode 100644 tests/regressiontests/staticfiles_tests/apps/test/models.py create mode 100644 tests/regressiontests/staticfiles_tests/apps/test/otherdir/odfile.txt create mode 100644 tests/regressiontests/staticfiles_tests/apps/test/static/test/.hidden create mode 100644 tests/regressiontests/staticfiles_tests/apps/test/static/test/CVS create mode 100644 tests/regressiontests/staticfiles_tests/apps/test/static/test/backup~ create mode 100644 tests/regressiontests/staticfiles_tests/apps/test/static/test/file.txt create mode 100644 tests/regressiontests/staticfiles_tests/apps/test/static/test/file1.txt create mode 100644 tests/regressiontests/staticfiles_tests/apps/test/static/test/test.ignoreme create mode 100644 tests/regressiontests/staticfiles_tests/models.py create mode 100644 tests/regressiontests/staticfiles_tests/project/documents/subdir/test.txt create mode 100644 tests/regressiontests/staticfiles_tests/project/documents/test.txt create mode 100644 tests/regressiontests/staticfiles_tests/project/documents/test/file.txt create mode 100644 tests/regressiontests/staticfiles_tests/project/site_media/media/media-file.txt create mode 100644 tests/regressiontests/staticfiles_tests/project/site_media/static/test/storage.txt create mode 100644 tests/regressiontests/staticfiles_tests/tests.py create mode 100644 tests/regressiontests/staticfiles_tests/urls/__init__.py create mode 100644 tests/regressiontests/staticfiles_tests/urls/default.py create mode 100644 tests/regressiontests/staticfiles_tests/urls/helper.py diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 2dabf3b1e3..e9ab5c7c98 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -194,7 +194,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.contrib.auth.context_processors.auth', 'django.core.context_processors.debug', 'django.core.context_processors.i18n', - 'django.core.context_processors.media', + 'django.contrib.staticfiles.context_processors.staticfiles', # 'django.core.context_processors.request', 'django.contrib.messages.context_processors.messages', ) @@ -202,11 +202,6 @@ TEMPLATE_CONTEXT_PROCESSORS = ( # Output to use in template system for invalid (e.g. misspelled) variables. TEMPLATE_STRING_IF_INVALID = '' -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/media/' - # Default e-mail address to use for various automated correspondence from # the site managers. DEFAULT_FROM_EMAIL = 'webmaster@localhost' @@ -551,3 +546,34 @@ TEST_DATABASE_COLLATION = None # The list of directories to search for fixtures FIXTURE_DIRS = () + +############### +# STATICFILES # +############### + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/static/" +STATICFILES_ROOT = '' + +# URL that handles the static files served from STATICFILES_ROOT. +# Example: "http://media.lawrence.com/static/" +STATICFILES_URL = '/static/' + +# A list of locations of additional static files +STATICFILES_DIRS = () + +# The default file storage backend used during the build process +STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# URL prefix for admin media -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index 3c783d4565..7791b47cab 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -44,7 +44,7 @@ USE_I18N = True USE_L10N = True # Absolute path to the directory that holds media. -# Example: "/home/media/media.lawrence.com/" +# Example: "/home/media/media.lawrence.com/media/" MEDIA_ROOT = '' # URL that handles the media served from MEDIA_ROOT. Make sure to use a @@ -52,10 +52,29 @@ MEDIA_ROOT = '' # Examples: "http://media.lawrence.com", "http://example.com/media/" MEDIA_URL = '' -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/media/' +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/static/" +STATICFILES_ROOT = '' + +# URL that handles the static files served from STATICFILES_ROOT. +# Example: "http://static.lawrence.com/", "http://example.com/static/" +STATICFILES_URL = '/static/' + +# URL prefix for admin media -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# A list of locations of additional static files +STATICFILES_DIRS = () + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) # Make this unique, and don't share it with anybody. SECRET_KEY = '' @@ -89,6 +108,7 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', + 'django.contrib.staticfiles', # Uncomment the next line to enable the admin: # 'django.contrib.admin', # Uncomment the next line to enable admin documentation: diff --git a/django/contrib/staticfiles/__init__.py b/django/contrib/staticfiles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/staticfiles/context_processors.py b/django/contrib/staticfiles/context_processors.py new file mode 100644 index 0000000000..e1c8d8c8ed --- /dev/null +++ b/django/contrib/staticfiles/context_processors.py @@ -0,0 +1,7 @@ +from django.conf import settings + +def staticfiles(request): + return { + 'STATICFILES_URL': settings.STATICFILES_URL, + 'MEDIA_URL': settings.MEDIA_URL, + } diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py new file mode 100644 index 0000000000..7f0a497174 --- /dev/null +++ b/django/contrib/staticfiles/finders.py @@ -0,0 +1,254 @@ +import os +from django.conf import settings +from django.db import models +from django.core.exceptions import ImproperlyConfigured +from django.core.files.storage import default_storage, Storage, FileSystemStorage +from django.utils.datastructures import SortedDict +from django.utils.functional import memoize, LazyObject +from django.utils.importlib import import_module + +from django.contrib.staticfiles import utils +from django.contrib.staticfiles.storage import AppStaticStorage + +_finders = {} + + +class BaseFinder(object): + """ + A base file finder to be used for custom staticfiles finder classes. + + """ + def find(self, path, all=False): + """ + Given a relative file path this ought to find an + absolute file path. + + If the ``all`` parameter is ``False`` (default) only + the first found file path will be returned; if set + to ``True`` a list of all found files paths is returned. + """ + raise NotImplementedError() + + def list(self, ignore_patterns=[]): + """ + Given an optional list of paths to ignore, this should return + a three item iterable with path, prefix and a storage instance. + """ + raise NotImplementedError() + + +class FileSystemFinder(BaseFinder): + """ + A static files finder that uses the ``STATICFILES_DIRS`` setting + to locate files. + """ + storages = SortedDict() + locations = set() + + def __init__(self, apps=None, *args, **kwargs): + for root in settings.STATICFILES_DIRS: + if isinstance(root, (list, tuple)): + prefix, root = root + else: + prefix = '' + self.locations.add((prefix, root)) + # Don't initialize multiple storages for the same location + for prefix, root in self.locations: + self.storages[root] = FileSystemStorage(location=root) + super(FileSystemFinder, self).__init__(*args, **kwargs) + + def find(self, path, all=False): + """ + Looks for files in the extra media locations + as defined in ``STATICFILES_DIRS``. + """ + matches = [] + for prefix, root in self.locations: + matched_path = self.find_location(root, path, prefix) + if matched_path: + if not all: + return matched_path + matches.append(matched_path) + return matches + + def find_location(self, root, path, prefix=None): + """ + Find a requested static file in a location, returning the found + absolute path (or ``None`` if no match). + """ + if prefix: + prefix = '%s/' % prefix + if not path.startswith(prefix): + return None + path = path[len(prefix):] + path = os.path.join(root, path) + if os.path.exists(path): + return path + + def list(self, ignore_patterns): + """ + List all files in all locations. + """ + for prefix, root in self.locations: + storage = self.storages[root] + for path in utils.get_files(storage, ignore_patterns): + yield path, prefix, storage + + +class AppDirectoriesFinder(BaseFinder): + """ + A static files finder that looks in the ``media`` directory of each app. + """ + storages = {} + storage_class = AppStaticStorage + + def __init__(self, apps=None, *args, **kwargs): + if apps is not None: + self.apps = apps + else: + self.apps = models.get_apps() + for app in self.apps: + self.storages[app] = self.storage_class(app) + super(AppDirectoriesFinder, self).__init__(*args, **kwargs) + + def list(self, ignore_patterns): + """ + List all files in all app storages. + """ + for storage in self.storages.itervalues(): + if storage.exists(''): # check if storage location exists + prefix = storage.get_prefix() + for path in utils.get_files(storage, ignore_patterns): + yield path, prefix, storage + + def find(self, path, all=False): + """ + Looks for files in the app directories. + """ + matches = [] + for app in self.apps: + app_matches = self.find_in_app(app, path) + if app_matches: + if not all: + return app_matches + matches.append(app_matches) + return matches + + def find_in_app(self, app, path): + """ + Find a requested static file in an app's media locations. + """ + storage = self.storages[app] + prefix = storage.get_prefix() + if prefix: + prefix = '%s/' % prefix + if not path.startswith(prefix): + return None + path = path[len(prefix):] + # only try to find a file if the source dir actually exists + if storage.exists(path): + matched_path = storage.path(path) + if matched_path: + return matched_path + + +class BaseStorageFinder(BaseFinder): + """ + A base static files finder to be used to extended + with an own storage class. + """ + storage = None + + def __init__(self, storage=None, *args, **kwargs): + if storage is not None: + self.storage = storage + if self.storage is None: + raise ImproperlyConfigured("The staticfiles storage finder %r " + "doesn't have a storage class " + "assigned." % self.__class__) + # Make sure we have an storage instance here. + if not isinstance(self.storage, (Storage, LazyObject)): + self.storage = self.storage() + super(BaseStorageFinder, self).__init__(*args, **kwargs) + + def find(self, path, all=False): + """ + Looks for files in the default file storage, if it's local. + """ + try: + self.storage.path('') + except NotImplementedError: + pass + else: + if self.storage.exists(path): + match = self.storage.path(path) + if all: + match = [match] + return match + return [] + + def list(self, ignore_patterns): + """ + List all files of the storage. + """ + for path in utils.get_files(self.storage, ignore_patterns): + yield path, '', self.storage + +class DefaultStorageFinder(BaseStorageFinder): + """ + A static files finder that uses the default storage backend. + """ + storage = default_storage + + +def find(path, all=False): + """ + Find a requested static file, first looking in any defined extra media + locations and next in any (non-excluded) installed apps. + + If no matches are found and the static location is local, look for a match + there too. + + If ``all`` is ``False`` (default), return the first matching + absolute path (or ``None`` if no match). Otherwise return a list of + found absolute paths. + + """ + matches = [] + for finder in get_finders(): + result = finder.find(path, all=all) + if not all and result: + return result + if not isinstance(result, (list, tuple)): + result = [result] + matches.extend(result) + if matches: + return matches + # No match. + return all and [] or None + +def get_finders(): + for finder_path in settings.STATICFILES_FINDERS: + yield get_finder(finder_path) + +def _get_finder(import_path): + """ + Imports the message storage class described by import_path, where + import_path is the full Python path to the class. + """ + module, attr = import_path.rsplit('.', 1) + try: + mod = import_module(module) + except ImportError, e: + raise ImproperlyConfigured('Error importing module %s: "%s"' % + (module, e)) + try: + Finder = getattr(mod, attr) + except AttributeError: + raise ImproperlyConfigured('Module "%s" does not define a "%s" ' + 'class.' % (module, attr)) + if not issubclass(Finder, BaseFinder): + raise ImproperlyConfigured('Finder "%s" is not a subclass of "%s"' % + (Finder, BaseFinder)) + return Finder() +get_finder = memoize(_get_finder, _finders, 1) diff --git a/django/contrib/staticfiles/handlers.py b/django/contrib/staticfiles/handlers.py new file mode 100644 index 0000000000..8681c55c16 --- /dev/null +++ b/django/contrib/staticfiles/handlers.py @@ -0,0 +1,72 @@ +import os +import urllib +from urlparse import urlparse + +from django.core.handlers.wsgi import WSGIHandler, STATUS_CODE_TEXT +from django.http import Http404 + +from django.contrib.staticfiles.views import serve + +class StaticFilesHandler(WSGIHandler): + """ + WSGI middleware that intercepts calls to the static files directory, as + defined by the STATICFILES_URL setting, and serves those files. + """ + def __init__(self, application, media_dir=None): + self.application = application + if media_dir: + self.media_dir = media_dir + else: + self.media_dir = self.get_media_dir() + self.media_url = self.get_media_url() + + def get_media_dir(self): + from django.conf import settings + return settings.STATICFILES_ROOT + + def get_media_url(self): + from django.conf import settings + return settings.STATICFILES_URL + + def file_path(self, url): + """ + Returns the relative path to the media file on disk for the given URL. + + The passed URL is assumed to begin with ``media_url``. If the + resultant file path is outside the media directory, then a ValueError + is raised. + """ + # Remove ``media_url``. + relative_url = url[len(self.media_url):] + return urllib.url2pathname(relative_url) + + def serve(self, request, path): + from django.contrib.staticfiles import finders + absolute_path = finders.find(path) + if not absolute_path: + raise Http404('%r could not be matched to a static file.' % path) + absolute_path, filename = os.path.split(absolute_path) + return serve(request, path=filename, document_root=absolute_path) + + def __call__(self, environ, start_response): + media_url_bits = urlparse(self.media_url) + # Ignore all requests if the host is provided as part of the media_url. + # Also ignore requests that aren't under the media path. + if (media_url_bits[1] or + not environ['PATH_INFO'].startswith(media_url_bits[2])): + return self.application(environ, start_response) + request = self.application.request_class(environ) + try: + response = self.serve(request, self.file_path(environ['PATH_INFO'])) + except Http404: + status = '404 NOT FOUND' + start_response(status, {'Content-type': 'text/plain'}.items()) + return [str('Page not found: %s' % environ['PATH_INFO'])] + status_text = STATUS_CODE_TEXT[response.status_code] + status = '%s %s' % (response.status_code, status_text) + response_headers = [(str(k), str(v)) for k, v in response.items()] + for c in response.cookies.values(): + response_headers.append(('Set-Cookie', str(c.output(header='')))) + start_response(status, response_headers) + return response + diff --git a/django/contrib/staticfiles/management/__init__.py b/django/contrib/staticfiles/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/staticfiles/management/commands/__init__.py b/django/contrib/staticfiles/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py new file mode 100644 index 0000000000..aae6bee10e --- /dev/null +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -0,0 +1,184 @@ +import os +import sys +import shutil +from optparse import make_option + +from django.conf import settings +from django.core.files.storage import get_storage_class +from django.core.management.base import CommandError, NoArgsCommand + +from django.contrib.staticfiles import finders + +class Command(NoArgsCommand): + """ + Command that allows to copy or symlink media files from different + locations to the settings.STATICFILES_ROOT. + """ + option_list = NoArgsCommand.option_list + ( + make_option('--noinput', action='store_false', dest='interactive', + default=True, help="Do NOT prompt the user for input of any " + "kind."), + make_option('-i', '--ignore', action='append', default=[], + dest='ignore_patterns', metavar='PATTERN', + help="Ignore files or directories matching this glob-style " + "pattern. Use multiple times to ignore more."), + make_option('-n', '--dry-run', action='store_true', dest='dry_run', + default=False, help="Do everything except modify the filesystem."), + make_option('-l', '--link', action='store_true', dest='link', + default=False, help="Create a symbolic link to each file instead of copying."), + make_option('--no-default-ignore', action='store_false', + dest='use_default_ignore_patterns', default=True, + help="Don't ignore the common private glob-style patterns 'CVS', " + "'.*' and '*~'."), + ) + help = "Collect static files from apps and other locations in a single location." + + def handle_noargs(self, **options): + symlink = options['link'] + ignore_patterns = options['ignore_patterns'] + if options['use_default_ignore_patterns']: + ignore_patterns += ['CVS', '.*', '*~'] + ignore_patterns = list(set(ignore_patterns)) + self.copied_files = [] + self.symlinked_files = [] + self.unmodified_files = [] + self.destination_storage = get_storage_class(settings.STATICFILES_STORAGE)() + + try: + self.destination_storage.path('') + except NotImplementedError: + self.destination_local = False + else: + self.destination_local = True + + if symlink: + if sys.platform == 'win32': + raise CommandError("Symlinking is not supported by this " + "platform (%s)." % sys.platform) + if not self.destination_local: + raise CommandError("Can't symlink to a remote destination.") + + # Warn before doing anything more. + if options.get('interactive'): + confirm = raw_input(""" +You have requested to collate static files and collect them at the destination +location as specified in your settings file. + +This will overwrite existing files. +Are you sure you want to do this? + +Type 'yes' to continue, or 'no' to cancel: """) + if confirm != 'yes': + raise CommandError("Static files build cancelled.") + + for finder in finders.get_finders(): + for source, prefix, storage in finder.list(ignore_patterns): + self.copy_file(source, prefix, storage, **options) + + verbosity = int(options.get('verbosity', 1)) + actual_count = len(self.copied_files) + len(self.symlinked_files) + unmodified_count = len(self.unmodified_files) + if verbosity >= 1: + self.stdout.write("\n%s static file%s %s to '%s'%s.\n" + % (actual_count, actual_count != 1 and 's' or '', + symlink and 'symlinked' or 'copied', + settings.STATICFILES_ROOT, + unmodified_count and ' (%s unmodified)' + % unmodified_count or '')) + + def copy_file(self, source, prefix, source_storage, **options): + """ + Attempt to copy (or symlink) ``source`` to ``destination``, + returning True if successful. + """ + source_path = source_storage.path(source) + try: + source_last_modified = source_storage.modified_time(source) + except (OSError, NotImplementedError): + source_last_modified = None + if prefix: + destination = '/'.join([prefix, source]) + else: + destination = source + symlink = options['link'] + dry_run = options['dry_run'] + verbosity = int(options.get('verbosity', 1)) + + if destination in self.copied_files: + if verbosity >= 2: + self.stdout.write("Skipping '%s' (already copied earlier)\n" + % destination) + return False + + if destination in self.symlinked_files: + if verbosity >= 2: + self.stdout.write("Skipping '%s' (already linked earlier)\n" + % destination) + return False + + if self.destination_storage.exists(destination): + try: + destination_last_modified = \ + self.destination_storage.modified_time(destination) + except (OSError, NotImplementedError): + # storage doesn't support ``modified_time`` or failed. + pass + else: + destination_is_link= os.path.islink( + self.destination_storage.path(destination)) + if destination_last_modified == source_last_modified: + if (not symlink and not destination_is_link): + if verbosity >= 2: + self.stdout.write("Skipping '%s' (not modified)\n" + % destination) + self.unmodified_files.append(destination) + return False + if dry_run: + if verbosity >= 2: + self.stdout.write("Pretending to delete '%s'\n" + % destination) + else: + if verbosity >= 2: + self.stdout.write("Deleting '%s'\n" % destination) + self.destination_storage.delete(destination) + + if symlink: + destination_path = self.destination_storage.path(destination) + if dry_run: + if verbosity >= 1: + self.stdout.write("Pretending to symlink '%s' to '%s'\n" + % (source_path, destination_path)) + else: + if verbosity >= 1: + self.stdout.write("Symlinking '%s' to '%s'\n" + % (source_path, destination_path)) + try: + os.makedirs(os.path.dirname(destination_path)) + except OSError: + pass + os.symlink(source_path, destination_path) + self.symlinked_files.append(destination) + else: + if dry_run: + if verbosity >= 1: + self.stdout.write("Pretending to copy '%s' to '%s'\n" + % (source_path, destination)) + else: + if self.destination_local: + destination_path = self.destination_storage.path(destination) + try: + os.makedirs(os.path.dirname(destination_path)) + except OSError: + pass + shutil.copy2(source_path, destination_path) + if verbosity >= 1: + self.stdout.write("Copying '%s' to '%s'\n" + % (source_path, destination_path)) + else: + source_file = source_storage.open(source) + self.destination_storage.save(destination, source_file) + if verbosity >= 1: + self.stdout.write("Copying %s to %s\n" + % (source_path, destination)) + self.copied_files.append(destination) + return True diff --git a/django/contrib/staticfiles/management/commands/findstatic.py b/django/contrib/staticfiles/management/commands/findstatic.py new file mode 100644 index 0000000000..0f13277c37 --- /dev/null +++ b/django/contrib/staticfiles/management/commands/findstatic.py @@ -0,0 +1,24 @@ +import os +from optparse import make_option +from django.core.management.base import LabelCommand + +from django.contrib.staticfiles import finders + +class Command(LabelCommand): + help = "Finds the absolute paths for the given static file(s)." + args = "[file ...]" + label = 'static file' + option_list = LabelCommand.option_list + ( + make_option('--first', action='store_false', dest='all', default=True, + help="Only return the first match for each static file."), + ) + + def handle_label(self, path, **options): + verbosity = int(options.get('verbosity', 1)) + result = finders.find(path, all=options['all']) + if result: + output = '\n '.join((os.path.realpath(path) for path in result)) + self.stdout.write("Found %r here:\n %s\n" % (path, output)) + else: + if verbosity >= 1: + self.stdout.write("No matching file found for %r.\n" % path) diff --git a/django/contrib/staticfiles/models.py b/django/contrib/staticfiles/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py new file mode 100644 index 0000000000..87b569af76 --- /dev/null +++ b/django/contrib/staticfiles/storage.py @@ -0,0 +1,84 @@ +import os +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.files.storage import FileSystemStorage +from django.utils.importlib import import_module + +from django.contrib.staticfiles import utils + + +class StaticFilesStorage(FileSystemStorage): + """ + Standard file system storage for site media files. + + The defaults for ``location`` and ``base_url`` are + ``STATICFILES_ROOT`` and ``STATICFILES_URL``. + """ + def __init__(self, location=None, base_url=None, *args, **kwargs): + if location is None: + location = settings.STATICFILES_ROOT + if base_url is None: + base_url = settings.STATICFILES_URL + if not location: + raise ImproperlyConfigured("You're using the staticfiles app " + "without having set the STATICFILES_ROOT setting. Set it to " + "the absolute path of the directory that holds static media.") + if not base_url: + raise ImproperlyConfigured("You're using the staticfiles app " + "without having set the STATICFILES_URL setting. Set it to " + "URL that handles the files served from STATICFILES_ROOT.") + super(StaticFilesStorage, self).__init__(location, base_url, *args, **kwargs) + + +class AppStaticStorage(FileSystemStorage): + """ + A file system storage backend that takes an app module and works + for the ``static`` directory of it. + """ + source_dir = 'static' + + def __init__(self, app, *args, **kwargs): + """ + Returns a static file storage if available in the given app. + """ + # app is actually the models module of the app. Remove the '.models'. + bits = app.__name__.split('.')[:-1] + self.app_name = bits[-1] + self.app_module = '.'.join(bits) + # The models module (app) may be a package in which case + # dirname(app.__file__) would be wrong. Import the actual app + # as opposed to the models module. + app = import_module(self.app_module) + location = self.get_location(os.path.dirname(app.__file__)) + super(AppStaticStorage, self).__init__(location, *args, **kwargs) + + def get_location(self, app_root): + """ + Given the app root, return the location of the static files of an app, + by default 'static'. We special case the admin app here since it has + its static files in 'media'. + """ + if self.app_module == 'django.contrib.admin': + return os.path.join(app_root, 'media') + return os.path.join(app_root, self.source_dir) + + def get_prefix(self): + """ + Return the path name that should be prepended to files for this app. + """ + if self.app_module == 'django.contrib.admin': + return self.app_name + return None + + def get_files(self, ignore_patterns=[]): + """ + Return a list containing the relative source paths for all files that + should be copied for an app. + """ + files = [] + prefix = self.get_prefix() + for path in utils.get_files(self, ignore_patterns): + if prefix: + path = '/'.join([prefix, path]) + files.append(path) + return files diff --git a/django/contrib/staticfiles/templatetags/__init__.py b/django/contrib/staticfiles/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/staticfiles/templatetags/staticfiles.py b/django/contrib/staticfiles/templatetags/staticfiles.py new file mode 100644 index 0000000000..6153f5a385 --- /dev/null +++ b/django/contrib/staticfiles/templatetags/staticfiles.py @@ -0,0 +1,43 @@ +from django import template +from django.utils.encoding import iri_to_uri + +register = template.Library() + +class StaticFilesPrefixNode(template.Node): + + def __init__(self, varname=None): + self.varname = varname + + def render(self, context): + try: + from django.conf import settings + except ImportError: + prefix = '' + else: + prefix = iri_to_uri(settings.STATICFILES_URL) + if self.varname is None: + return prefix + context[self.varname] = prefix + return '' + +@register.tag +def get_staticfiles_prefix(parser, token): + """ + Populates a template variable with the prefix (settings.STATICFILES_URL). + + Usage:: + + {% get_staticfiles_prefix [as varname] %} + + Examples:: + + {% get_staticfiles_prefix %} + {% get_staticfiles_prefix as staticfiles_prefix %} + + """ + tokens = token.contents.split() + if len(tokens) > 1 and tokens[1] != 'as': + raise template.TemplateSyntaxError( + "First argument in '%s' must be 'as'" % tokens[0]) + return StaticFilesPrefixNode(varname=(len(tokens) > 1 and tokens[2] or None)) + diff --git a/django/contrib/staticfiles/urls.py b/django/contrib/staticfiles/urls.py new file mode 100644 index 0000000000..131b10266b --- /dev/null +++ b/django/contrib/staticfiles/urls.py @@ -0,0 +1,29 @@ +import re +from django.conf import settings +from django.conf.urls.defaults import patterns, url, include +from django.core.exceptions import ImproperlyConfigured + +urlpatterns = [] + +# only serve non-fqdn URLs +if not settings.DEBUG: + urlpatterns += patterns('', + url(r'^(?P.*)$', 'django.contrib.staticfiles.views.serve'), + ) + +def staticfiles_urlpatterns(prefix=None): + """ + Helper function to return a URL pattern for serving static files. + """ + if settings.DEBUG: + return [] + if prefix is None: + prefix = settings.STATICFILES_URL + if '://' in prefix: + raise ImproperlyConfigured( + "The STATICFILES_URL setting is a full URL, not a path and " + "can't be used with the urls.staticfiles_urlpatterns() helper.") + if prefix.startswith("/"): + prefix = prefix[1:] + return patterns('', + url(r'^%s' % re.escape(prefix), include(urlpatterns)),) diff --git a/django/contrib/staticfiles/utils.py b/django/contrib/staticfiles/utils.py new file mode 100644 index 0000000000..f5a30befe9 --- /dev/null +++ b/django/contrib/staticfiles/utils.py @@ -0,0 +1,30 @@ +import fnmatch + +def get_files(storage, ignore_patterns=[], location=''): + """ + Recursively walk the storage directories gathering a complete list of files + that should be copied, returning this list. + + """ + def is_ignored(path): + """ + Return True or False depending on whether the ``path`` should be + ignored (if it matches any pattern in ``ignore_patterns``). + + """ + for pattern in ignore_patterns: + if fnmatch.fnmatchcase(path, pattern): + return True + return False + + directories, files = storage.listdir(location) + static_files = [location and '/'.join([location, fn]) or fn + for fn in files + if not is_ignored(fn)] + for dir in directories: + if is_ignored(dir): + continue + if location: + dir = '/'.join([location, dir]) + static_files.extend(get_files(storage, ignore_patterns, dir)) + return static_files diff --git a/django/contrib/staticfiles/views.py b/django/contrib/staticfiles/views.py new file mode 100644 index 0000000000..27d499df87 --- /dev/null +++ b/django/contrib/staticfiles/views.py @@ -0,0 +1,159 @@ +""" +Views and functions for serving static files. These are only to be used during +development, and SHOULD NOT be used in a production setting. + +""" +import mimetypes +import os +import posixpath +import re +import stat +import urllib +from email.Utils import parsedate_tz, mktime_tz + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified +from django.template import loader, Template, Context, TemplateDoesNotExist +from django.utils.http import http_date + +from django.contrib.staticfiles import finders + + +def serve(request, path, document_root=None, show_indexes=False): + """ + Serve static files below a given point in the directory structure or + from locations inferred from the static files finders. + + To use, put a URL pattern such as:: + + (r'^(?P.*)$', 'django.contrib.staticfiles.views.serve') + + in your URLconf. + + If you provide the ``document_root`` parameter, the file won't be looked + up with the staticfiles finders, but in the given filesystem path, e.g.:: + + (r'^(?P.*)$', 'django.contrib.staticfiles.views.serve', {'document_root' : '/path/to/my/files/'}) + + You may also set ``show_indexes`` to ``True`` if you'd like to serve a + basic index of the directory. This index view will use the + template hardcoded below, but if you'd like to override it, you can create + a template called ``static/directory_index.html``. + """ + if settings.DEBUG: + raise ImproperlyConfigured("The view to serve static files can only " + "be used if the DEBUG setting is True") + if not document_root: + absolute_path = finders.find(path) + if not absolute_path: + raise Http404("%r could not be matched to a static file." % path) + document_root, path = os.path.split(absolute_path) + # Clean up given path to only allow serving files below document_root. + path = posixpath.normpath(urllib.unquote(path)) + path = path.lstrip('/') + newpath = '' + for part in path.split('/'): + if not part: + # Strip empty path components. + continue + drive, part = os.path.splitdrive(part) + head, part = os.path.split(part) + if part in (os.curdir, os.pardir): + # Strip '.' and '..' in path. + continue + newpath = os.path.join(newpath, part).replace('\\', '/') + if newpath and path != newpath: + return HttpResponseRedirect(newpath) + fullpath = os.path.join(document_root, newpath) + if os.path.isdir(fullpath): + if show_indexes: + return directory_index(newpath, fullpath) + raise Http404("Directory indexes are not allowed here.") + if not os.path.exists(fullpath): + raise Http404('"%s" does not exist' % fullpath) + # Respect the If-Modified-Since header. + statobj = os.stat(fullpath) + mimetype, encoding = mimetypes.guess_type(fullpath) + mimetype = mimetype or 'application/octet-stream' + if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), + statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]): + return HttpResponseNotModified(mimetype=mimetype) + contents = open(fullpath, 'rb').read() + response = HttpResponse(contents, mimetype=mimetype) + response["Last-Modified"] = http_date(statobj[stat.ST_MTIME]) + response["Content-Length"] = len(contents) + if encoding: + response["Content-Encoding"] = encoding + return response + + +DEFAULT_DIRECTORY_INDEX_TEMPLATE = """ + + + + + + + Index of {{ directory }} + + +

Index of {{ directory }}

+
    + {% ifnotequal directory "/" %} +
  • ../
  • + {% endifnotequal %} + {% for f in file_list %} +
  • {{ f }}
  • + {% endfor %} +
+ + +""" + +def directory_index(path, fullpath): + try: + t = loader.select_template(['static/directory_index.html', + 'static/directory_index']) + except TemplateDoesNotExist: + t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template') + files = [] + for f in os.listdir(fullpath): + if not f.startswith('.'): + if os.path.isdir(os.path.join(fullpath, f)): + f += '/' + files.append(f) + c = Context({ + 'directory' : path + '/', + 'file_list' : files, + }) + return HttpResponse(t.render(c)) + +def was_modified_since(header=None, mtime=0, size=0): + """ + Was something modified since the user last downloaded it? + + header + This is the value of the If-Modified-Since header. If this is None, + I'll just return True. + + mtime + This is the modification time of the item we're talking about. + + size + This is the size of the item we're talking about. + """ + try: + if header is None: + raise ValueError + matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header, + re.IGNORECASE) + header_mtime = mktime_tz(parsedate_tz(matches.group(1))) + header_len = matches.group(3) + if header_len and int(header_len) != size: + raise ValueError + if mtime > header_mtime: + raise ValueError + except (AttributeError, ValueError, OverflowError): + return True + return False diff --git a/django/core/context_processors.py b/django/core/context_processors.py index 7a59728bfc..a529c612aa 100644 --- a/django/core/context_processors.py +++ b/django/core/context_processors.py @@ -71,7 +71,15 @@ def media(request): Adds media-related context variables to the context. """ - return {'MEDIA_URL': settings.MEDIA_URL} + import warnings + warnings.warn( + "The context processor at `django.core.context_processors.media` is " \ + "deprecated; use the path `django.contrib.staticfiles.context_processors.staticfiles` " \ + "instead.", + PendingDeprecationWarning + ) + from django.contrib.staticfiles.context_processors import staticfiles as staticfiles_context_processor + return staticfiles_context_processor(request) def request(request): return {'request': request} diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index fc2c694ab6..21391e82ad 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -1,7 +1,9 @@ -from django.core.management.base import BaseCommand, CommandError from optparse import make_option import os import sys +import warnings + +from django.core.management.base import BaseCommand, CommandError class Command(BaseCommand): option_list = BaseCommand.option_list + ( @@ -20,6 +22,7 @@ class Command(BaseCommand): import django from django.core.servers.basehttp import run, AdminMediaHandler, WSGIServerException from django.core.handlers.wsgi import WSGIHandler + from django.contrib.staticfiles.handlers import StaticFilesHandler if args: raise CommandError('Usage is runserver %s' % self.args) if not addrport: @@ -56,7 +59,10 @@ class Command(BaseCommand): translation.activate(settings.LANGUAGE_CODE) try: - handler = AdminMediaHandler(WSGIHandler(), admin_media_path) + handler = WSGIHandler() + handler = StaticFilesHandler(handler) + # serve admin media like old-school (deprecation pending) + handler = AdminMediaHandler(handler, admin_media_path) run(addr, int(port), handler) except WSGIServerException, e: # Use helpful error messages instead of ugly tracebacks. diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index dae4297379..ab38e98f13 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -8,16 +8,17 @@ been reviewed for security issues. Don't use it for production use. """ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -import mimetypes import os import re -import stat import sys import urllib +import warnings from django.core.management.color import color_style from django.utils.http import http_date from django.utils._os import safe_join +from django.contrib.staticfiles.handlers import StaticFilesHandler +from django.views import static __version__ = "0.1" __all__ = ['WSGIServer','WSGIRequestHandler'] @@ -633,86 +634,46 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): sys.stderr.write(msg) -class AdminMediaHandler(object): + +class AdminMediaHandler(StaticFilesHandler): """ WSGI middleware that intercepts calls to the admin media directory, as defined by the ADMIN_MEDIA_PREFIX setting, and serves those images. Use this ONLY LOCALLY, for development! This hasn't been tested for security and is not super efficient. """ - def __init__(self, application, media_dir=None): + + def get_media_dir(self): + import django + return os.path.join(django.__path__[0], 'contrib', 'admin', 'media') + + def get_media_url(self): from django.conf import settings - self.application = application - if not media_dir: - import django - self.media_dir = \ - os.path.join(django.__path__[0], 'contrib', 'admin', 'media') - else: - self.media_dir = media_dir - self.media_url = settings.ADMIN_MEDIA_PREFIX + return settings.ADMIN_MEDIA_PREFIX + + def __init__(self, application, media_dir=None): + warnings.warn('The AdminMediaHandler handler is deprecated; use the ' + '`django.contrib.staticfiles.handlers.StaticFilesHandler` instead.', + PendingDeprecationWarning) + super(AdminMediaHandler, self).__init__(application, media_dir) def file_path(self, url): """ Returns the path to the media file on disk for the given URL. - The passed URL is assumed to begin with ADMIN_MEDIA_PREFIX. If the + The passed URL is assumed to begin with ``media_url``. If the resultant file path is outside the media directory, then a ValueError is raised. """ - # Remove ADMIN_MEDIA_PREFIX. + # Remove ``media_url``. relative_url = url[len(self.media_url):] relative_path = urllib.url2pathname(relative_url) return safe_join(self.media_dir, relative_path) - def __call__(self, environ, start_response): - import os.path + def serve(self, request, path): + document_root, path = os.path.split(path) + return static.serve(request, path, document_root=document_root) - # Ignore requests that aren't under ADMIN_MEDIA_PREFIX. Also ignore - # all requests if ADMIN_MEDIA_PREFIX isn't a relative URL. - if self.media_url.startswith('http://') or self.media_url.startswith('https://') \ - or not environ['PATH_INFO'].startswith(self.media_url): - return self.application(environ, start_response) - - # Find the admin file and serve it up, if it exists and is readable. - try: - file_path = self.file_path(environ['PATH_INFO']) - except ValueError: # Resulting file path was not valid. - status = '404 NOT FOUND' - headers = {'Content-type': 'text/plain'} - output = ['Page not found: %s' % environ['PATH_INFO']] - start_response(status, headers.items()) - return output - if not os.path.exists(file_path): - status = '404 NOT FOUND' - headers = {'Content-type': 'text/plain'} - output = ['Page not found: %s' % environ['PATH_INFO']] - else: - try: - fp = open(file_path, 'rb') - except IOError: - status = '401 UNAUTHORIZED' - headers = {'Content-type': 'text/plain'} - output = ['Permission denied: %s' % environ['PATH_INFO']] - else: - # This is a very simple implementation of conditional GET with - # the Last-Modified header. It makes media files a bit speedier - # because the files are only read off disk for the first - # request (assuming the browser/client supports conditional - # GET). - mtime = http_date(os.stat(file_path)[stat.ST_MTIME]) - headers = {'Last-Modified': mtime} - if environ.get('HTTP_IF_MODIFIED_SINCE', None) == mtime: - status = '304 NOT MODIFIED' - output = [] - else: - status = '200 OK' - mime_type = mimetypes.guess_type(file_path)[0] - if mime_type: - headers['Content-Type'] = mime_type - output = [fp.read()] - fp.close() - start_response(status, headers.items()) - return output def run(addr, port, wsgi_handler): server_address = (addr, port) diff --git a/django/views/static.py b/django/views/static.py index cc88211876..eee9885c02 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -9,6 +9,7 @@ import posixpath import re import stat import urllib +import warnings from email.Utils import parsedate_tz, mktime_tz from django.template import loader @@ -16,6 +17,10 @@ from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpRespons from django.template import Template, Context, TemplateDoesNotExist from django.utils.http import http_date +from django.contrib.staticfiles.views import \ + directory_index, was_modified_since, serve as staticfiles_serve + + def serve(request, path, document_root=None, show_indexes=False): """ Serve static files below a given point in the directory structure. @@ -30,111 +35,7 @@ def serve(request, path, document_root=None, show_indexes=False): but if you'd like to override it, you can create a template called ``static/directory_index.html``. """ - - # Clean up given path to only allow serving files below document_root. - path = posixpath.normpath(urllib.unquote(path)) - path = path.lstrip('/') - newpath = '' - for part in path.split('/'): - if not part: - # Strip empty path components. - continue - drive, part = os.path.splitdrive(part) - head, part = os.path.split(part) - if part in (os.curdir, os.pardir): - # Strip '.' and '..' in path. - continue - newpath = os.path.join(newpath, part).replace('\\', '/') - if newpath and path != newpath: - return HttpResponseRedirect(newpath) - fullpath = os.path.join(document_root, newpath) - if os.path.isdir(fullpath): - if show_indexes: - return directory_index(newpath, fullpath) - raise Http404("Directory indexes are not allowed here.") - if not os.path.exists(fullpath): - raise Http404('"%s" does not exist' % fullpath) - # Respect the If-Modified-Since header. - statobj = os.stat(fullpath) - mimetype, encoding = mimetypes.guess_type(fullpath) - mimetype = mimetype or 'application/octet-stream' - if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), - statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]): - return HttpResponseNotModified(mimetype=mimetype) - contents = open(fullpath, 'rb').read() - response = HttpResponse(contents, mimetype=mimetype) - response["Last-Modified"] = http_date(statobj[stat.ST_MTIME]) - response["Content-Length"] = len(contents) - if encoding: - response["Content-Encoding"] = encoding - return response - -DEFAULT_DIRECTORY_INDEX_TEMPLATE = """ - - - - - - - Index of {{ directory }} - - -

Index of {{ directory }}

-
    - {% ifnotequal directory "/" %} -
  • ../
  • - {% endifnotequal %} - {% for f in file_list %} -
  • {{ f }}
  • - {% endfor %} -
- - -""" - -def directory_index(path, fullpath): - try: - t = loader.select_template(['static/directory_index.html', - 'static/directory_index']) - except TemplateDoesNotExist: - t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name='Default directory index template') - files = [] - for f in os.listdir(fullpath): - if not f.startswith('.'): - if os.path.isdir(os.path.join(fullpath, f)): - f += '/' - files.append(f) - c = Context({ - 'directory' : path + '/', - 'file_list' : files, - }) - return HttpResponse(t.render(c)) - -def was_modified_since(header=None, mtime=0, size=0): - """ - Was something modified since the user last downloaded it? - - header - This is the value of the If-Modified-Since header. If this is None, - I'll just return True. - - mtime - This is the modification time of the item we're talking about. - - size - This is the size of the item we're talking about. - """ - try: - if header is None: - raise ValueError - matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header, - re.IGNORECASE) - header_mtime = mktime_tz(parsedate_tz(matches.group(1))) - header_len = matches.group(3) - if header_len and int(header_len) != size: - raise ValueError - if mtime > header_mtime: - raise ValueError - except (AttributeError, ValueError, OverflowError): - return True - return False + warnings.warn("The view at `django.views.static.serve` is deprecated; " + "use the path `django.contrib.staticfiles.views.serve` " + "instead.", PendingDeprecationWarning) + return staticfiles_serve(request, path, document_root, show_indexes) diff --git a/docs/howto/static-files.txt b/docs/howto/static-files.txt index 209cf38c36..3b5b7a5683 100644 --- a/docs/howto/static-files.txt +++ b/docs/howto/static-files.txt @@ -1,162 +1,399 @@ -========================= -How to serve static files -========================= +===================== +Managing static files +===================== -.. module:: django.views.static - :synopsis: Serving of static files during development. +.. currentmodule:: django.contrib.staticfiles -Django itself doesn't serve static (media) files, such as images, style sheets, -or video. It leaves that job to whichever Web server you choose. +.. versionadded:: 1.3 -The reasoning here is that standard Web servers, such as Apache_, lighttpd_ and -Cherokee_, are much more fine-tuned at serving static files than a Web -application framework. +Django developers mostly concern themselves with the dynamic parts of web +applications -- the views and templates that render anew for each request. But +web applications have other parts: the static media files (images, CSS, +Javascript, etc.) that are needed to render a complete web page. -With that said, Django does support static files **during development**. You can -use the :func:`django.views.static.serve` view to serve media files. +For small projects, this isn't a big deal, because you can just keep the media +somewhere your web server can find it. However, in bigger projects -- especially +those comprised of multiple apps -- dealing with the multiple sets of static +files provided by each application starts to get tricky. -.. _Apache: http://httpd.apache.org/ -.. _lighttpd: http://www.lighttpd.net/ -.. _Cherokee: http://www.cherokee-project.com/ +That's what ``django.contrib.staticfiles`` is for: it collects media from each +of your applications (and any other places you specify) into a single location +that can easily be served in production. -.. seealso:: +.. note:: - If you just need to serve the admin media from a nonstandard location, see - the :djadminopt:`--adminmedia` parameter to :djadmin:`runserver`. + If you've used the `django-staticfiles`_ third-party app before, then + ``django.contrib.staticfiles`` will look very familiar. That's because + they're essentially the same code: ``django.contrib.staticfiles`` started + its life as `django-staticfiles`_ and was merged into Django 1.3. + + If you're upgrading from ``django-staticfiles``, please see `Upgrading from + django-staticfiles`_, below, for a few minor changes you'll need to make. -The big, fat disclaimer -======================= +.. _django-staticfiles: http://pypi.python.org/pypi/django-staticfiles/ -Using this method is **inefficient** and **insecure**. Do not use this in a -production setting. Use this only for development. +Using ``django.contrib.staticfiles`` +==================================== -For information on serving static files in an Apache production environment, -see the :ref:`Django mod_wsgi documentation `. +Here's the basic usage in a nutshell: -How to do it -============ + 1. Put your media somewhere that staticfiles will find it.. -Here's the formal definition of the :func:`~django.views.static.serve` view: + Most of the time this place will be in a ``static`` directory within your + application, but it could also be a specific directory you've put into + your settings file. See the the documentation for the + :setting:`STATICFILES_DIRS` and :setting:`STATICFILES_FINDERS` settings + for details on where you can put media. -.. function:: def serve(request, path, document_root, show_indexes=False) + 2. Add some ``staticfiles``-related settings to your settings file. -To use it, just put this in your :doc:`URLconf `:: + First, you'll need to make sure that ``django.contrib.staticfiles`` is in + your :setting:`INSTALLED_APPS`. - (r'^site_media/(?P.*)$', 'django.views.static.serve', - {'document_root': '/path/to/media'}), + Next, you'll need to edit :setting:`STATICFILES_ROOT` to point to where + you'd like your static media stored. For example:: -...where ``site_media`` is the URL where your media will be rooted, and -``/path/to/media`` is the filesystem root for your media. This will call the -:func:`~django.views.static.serve` view, passing in the path from the URLconf -and the (required) ``document_root`` parameter. + STATICFILES_ROOT = "/home/jacob/projects/mysite.com/static_media" -Given the above URLconf: + You may also want to set the :setting:`STATICFILES_URL` setting at this + time, though the default value (of ``/static/``) is perfect for local + development. - * The file ``/path/to/media/foo.jpg`` will be made available at the URL - ``/site_media/foo.jpg``. + There are a number of other options available that let you control *how* + media is stored, where ``staticfiles`` searches for files, and how files + will be served; see :ref:`the staticfiles settings reference + ` for details. - * The file ``/path/to/media/css/mystyles.css`` will be made available - at the URL ``/site_media/css/mystyles.css``. + 3. Run the :djadmin:`collectstatic` management command:: - * The file ``/path/bar.jpg`` will not be accessible, because it doesn't - fall under the document root. + ./manage.py collectstatic -Of course, it's not compulsory to use a fixed string for the -``'document_root'`` value. You might wish to make that an entry in your -settings file and use the setting value there. That will allow you and -other developers working on the code to easily change the value as -required. For example, if we have a line in ``settings.py`` that says:: + This'll churn through your static file storage and move them into the + directory given by :setting:`STATICFILES_ROOT`. - STATIC_DOC_ROOT = '/path/to/media' + 4. Deploy that media. -...we could write the above :doc:`URLconf ` entry as:: + If you're using the built-in development server, you can quickly + serve static media locally by adding:: - from django.conf import settings - ... - (r'^site_media/(?P.*)$', 'django.views.static.serve', - {'document_root': settings.STATIC_DOC_ROOT}), + from django.contrib.staticfiles.urls import staticfiles_urlpatterns + urlpatterns += staticfiles_urlpatterns() -Be careful not to use the same path as your :setting:`ADMIN_MEDIA_PREFIX` (which defaults -to ``/media/``) as this will overwrite your URLconf entry. + to the bottom of your URLconf. See :ref:`staticfiles-development` for + details. -Directory listings -================== + When it comes time to deploy to production, :ref:`staticfiles-production` + covers some common deployment strategies for static files. -Optionally, you can pass the ``show_indexes`` parameter to the -:func:`~django.views.static.serve` view. This is ``False`` by default. If it's -``True``, Django will display file listings for directories. + However you choose to deploy those files, you'll probably need to refer + to them in your templates. The easiest method is to use the included + context processor which will allow template code like: -For example:: + .. code-block:: html+django - (r'^site_media/(?P.*)$', 'django.views.static.serve', - {'document_root': '/path/to/media', 'show_indexes': True}), + + +Of course, there are some serious problems with this: it doesn't work well in +development, and it makes it *very* hard to change where you've deployed your +media. If, for example, you wanted to switch to using a content delivery network +(CDN), then you'd need to change more or less every single template. + +A far better way is to use the value of the :setting:`STATICFILES_URL` setting +directly in your templates. This means that a switch of media servers only +requires changing that single value. Much better! + +``staticfiles`` inludes two built-in ways of getting at this setting in your +templates: a context processor and a template tag. + +With a context processor +------------------------ + +The included context processor is the easy way. Simply make sure +``'django.contrib.staticfiles.context_processors.staticfiles'`` is in your +:setting:`TEMPLATE_CONTEXT_PROCESSORS`. It's there by default, and if you're +editing that setting by hand it should look something like:: + + TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'django.contrib.staticfiles.context_processors.staticfiles', + ) + +Once that's done, you can refer to :setting:`STATICFILES_URL` in your templates: .. code-block:: html+django - - - - - - - Index of {{ directory }} - - -

Index of {{ directory }}

-
    - {% for f in file_list %} -
  • {{ f }}
  • - {% endfor %} -
- - + - if settings.DEBUG: - urlpatterns += patterns('', - (r'^site_media/(?P.*)$', 'django.views.static.serve', {'document_root': '/path/to/media'}), +There's also a second form you can use to avoid extra processing if you need the +value multiple times: + +.. code-block:: html+django + + {% load staticfiles %} + {% get_staticfiles_prefix as STATIC_PREFIX %} + + + + +.. _staticfiles-development: + +Serving static files in development +=================================== + +The static files tools are mostly designed to help with getting static media +successfully deployed into production. This usually means a separate, dedicated +media server, which is a lot of overhead to mess with when developing locally. +Thus, the ``staticfiles`` app ships with a quick and dirty helper view that you +can use to serve media locally in development. + +To enable this view, you'll add a couple of lines to your URLconf. The first +line goes at the top of the file, and the last line at the bottom:: + + from django.contrib.staticfiles.urls import staticfiles_urlpatterns + + # ... the rest of your URLconf goes here ... + + urlpatterns += staticfiles_urlpatterns() + +This will inspect your :setting:`STATICFILES_URL` and +:setting:`STATICFILES_ROOT` settings and wire up the view to serve static media +accordingly. Remember to run :djadmin:`collectstatic` when your media changes; +the view only serves static files that have been collected. + +.. warning:: + + This will only work if :setting:`DEBUG` is ``True``. + + That's because this view is **grossly inefficient** and probably + **insecure**. This is only intended for local development, and should + **never be used in production**. + +For a few more details, including an alternate method of enabling this view, +see :ref:`staticfiles-development-view`. + +.. _staticfiles-production: + +Serving static files in production +================================== + +The basic outline of putting static files into production a simple: un the +:djadmin:`collectstatic` command when static media changes, then arrange for the +collected media directory (:setting:`STATICFILES_ROOT`) to be moved to the media +server and served. + +Of course, as with all deployment tasks, the devil's in the details. Every +production setup will be a bit different, so you'll need to adapt the basic +outline to fit your needs. Below are a few common patterns that might help. + +Serving the app and your static files from the same server +---------------------------------------------------------- + +If you want to serve your media from the same server that's already serving your +app, the basic outline gets modified to look something like: + + * Push your code up to the deployment server. + * On the server, run :djadmin:`collectmedia` to move all the media into + :setting:`STATICFILES_ROOT`. + * Point your web server at :setting:`STATICFILES_ROOT`. For example, here's + of :ref:`how to do this under Apache and mod_wsgi `. + +You'll probably want to automate this process, especially if you've got multiple +web servers. There's any number of ways to do this automation, but one option +that many Django developers enjoy is `Fabric`__. + +__ http://fabfile.org/ + +Below, and in the following sections, we'll show off a few example fabfiles +(i.e. Fabric scripts) that automate these media deployment options. The syntax +of a fabfile is fairly streightforward but won't be covered here; consult `Fabric's documentation`__, for a complete explanation of the syntax.. + +__ http://docs.fabfile.org/ + +So, a fabfile to deploy media to a couple of web servers might look something +like:: + + from fabric.api import * + + # Hosts to deploy onto + env.hosts = ['www1.example.com', 'www2.example.com'] + + # Where your project code lives on the server + env.project_root = '/home/www/myproject' + + def deploy_static(): + with cd(env.project_root): + run('./manage.py collectstatic') + +Serving static files from a dedicated media server +-------------------------------------------------- + +Most larger Django apps use a separate Web server -- i.e., one that's not also +running Django -- for serving media. This server often runs a different type of +web server -- faster but less full-featured. Some good choices are: + + * lighttpd_ + * Nginx_ + * TUX_ + * Cherokee_ + * A stripped-down version of Apache_ + +.. _lighttpd: http://www.lighttpd.net/ +.. _Nginx: http://wiki.nginx.org/Main +.. _TUX: http://en.wikipedia.org/wiki/TUX_web_server +.. _Apache: http://httpd.apache.org/ +.. _Cherokee: http://www.cherokee-project.com/ + +Configuring these servers is out of scope of this document; check each server's +respective documentation for instructions. + +Since your media server won't be running Django, you'll need to modify the +deployment strategy to look something like: + + * When your media changes, run :djadmin:`collectstatic` locally. + * Push your local :setting:`STATICFILES_ROOT` up to the media server + into the directory that's being served. ``rsync`` is a good + choice for this step since it only needs to transfer the + bits of static media that have changed. + +Here's how this might look in a fabfile:: + + from fabric.api import * + from fabric.contrib import project + + # Where the static files get collected locally + env.local_static_root = '/tmp/static' + + # Where the static files should go remotely + env.remote_static_root = '/home/www/media.example.com' + + @roles('media') + def deploy_static(): + local('./manage.py collectstatic') + project.rysnc_project( + remote_dir = env.remote_static_root, + local_dir = env.local_static_root, + delete = True ) -This code is straightforward. It imports the settings and checks the value of -the :setting:`DEBUG` setting. If it evaluates to ``True``, then ``site_media`` -will be associated with the ``django.views.static.serve`` view. If not, then the -view won't be made available. +.. _staticfiles-from-cdn: -Of course, the catch here is that you'll have to remember to set ``DEBUG=False`` -in your production settings file. But you should be doing that anyway. +Serving static media from a cloud service or CDN +------------------------------------------------ + +Another common tactic is to serve media from a cloud storage provider like +Amazon's S3__ and/or a CDN (content delivery network). This lets you ignore the +problems of serving media, and can often make for faster-loading webpages +(especially when using a CDN). + +When using these services, the basic workflow would look a bit like the above, +except that instead of using ``rsync`` to transfer your media to the server +you'd need to transfer the media to the storage provider or CDN. + +There's any number of ways you might do this, but if the provider has an API a +:doc:`custom file storage backend ` will make the +process incredibly simple. If you've written or are using a 3rd party custom +storage backend, you can tell :djadmin:`collectstatic` to use it by setting +:setting:`STATICFILES_STORAGE` to the storage engine. + +For example, if you've written an S3 storage backend in +``myproject.storage.S3Storage`` you could use it with:: + + STATICFILES_STORAGE = 'storages.backends.s3.S3Storage' + +Once that's done, all you have to do is run :djadmin:`collectstatic` and your +media would be pushed through your storage package up to S3. If you later needed +to swich to a different storage provider, it could be as simple as changing your +:setting:`STATICFILES_STORAGE` setting. + +For details on how you'd write one of these backends, +:doc:`/howto/custom-file-storage`. + +.. seealso:: + + The `django-storages`__ project is a 3rd party app that provides many + storage backends for many common file storage APIs (including S3). + +__ http://s3.amazonaws.com/ +__ http://code.welldev.org/django-storages/wiki/S3Storage + +Upgrading from ``django-staticfiles`` +===================================== + +``django.contrib.staticfiles`` began its life as `django-staticfiles`_. If +you're upgrading from `django-staticfiles`_ to ``django.contrib.staticfiles``, +you'll need to make a few changes: + + * Application files should now live in a ``static`` directory in each app + (`django-staticfiles`_ used the name ``media``, which was slightly + confusing). + + * The management commands ``build_static`` and ``resolve_static`` are now + called :djadmin:`collectstatic` and :djadmin:`findstatic`. + + * The settings ``STATIC_URL`` and ``STATIC_ROOT`` were renamed to + :setting:`STATICFILES_URL` and :setting:`STATICFILES_ROOT`. + + * The settings ``STATICFILES_PREPEND_LABEL_APPS``, + ``STATICFILES_MEDIA_DIRNAMES`` and ``STATICFILES_EXCLUDED_APPS`` were + removed. + + * The setting ``STATICFILES_RESOLVERS`` was removed, and replaced by the new + :setting:`STATICFILES_FINDERS`. + + * The default for :setting:`STATICFILES_STORAGE` was renamed from + ``staticfiles.storage.StaticFileStorage`` to + ``staticfiles.storage.StaticFilesStorage`` + +Learn more +========== + +This document has covered the basics and some common usage patterns. For +complete details on all the settings, commands, template tags, and other pieces +include in ``django.contrib.staticfiles``, see :doc:`the statcfiles reference +`. \ No newline at end of file diff --git a/docs/index.txt b/docs/index.txt index e456d047ec..afa2e37f25 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -155,7 +155,7 @@ The development process :doc:`Apache/mod_python ` | :doc:`FastCGI/SCGI/AJP ` | :doc:`Apache authentication ` | - :doc:`Serving static files ` | + :doc:`Handling static files ` | :doc:`Tracking code errors by e-mail ` Other batteries included @@ -185,6 +185,7 @@ Other batteries included * :doc:`Signals ` * :doc:`Sitemaps ` * :doc:`Sites ` + * :doc:`Static Files ` * :doc:`Syndication feeds (RSS/Atom) ` * :doc:`Unicode in Django ` * :doc:`Web design helpers ` diff --git a/docs/ref/contrib/index.txt b/docs/ref/contrib/index.txt index 90edf72c94..5e308dc469 100644 --- a/docs/ref/contrib/index.txt +++ b/docs/ref/contrib/index.txt @@ -38,6 +38,7 @@ those packages have. redirects sitemaps sites + staticfiles syndication webdesign diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt new file mode 100644 index 0000000000..0a330a0fce --- /dev/null +++ b/docs/ref/contrib/staticfiles.txt @@ -0,0 +1,283 @@ +=================== +The staticfiles app +=================== + +.. module:: django.contrib.staticfiles + :synopsis: An app for handling static files. + +.. versionadded:: 1.3 + +``django.contrib.staticfiles`` collects media from each of your applications +(and any other places you specify) into a single location that can easily be +served in production. + +.. seealso:: + + For an introduction to the static files app and some usage examples, see + :doc:`/howto/static-files`. + +.. _staticfiles-settings: + +Settings +======== + +.. highlight:: python + +The following settings control the behavior of the static files app. Only +:setting:`STATICFILES_ROOT` is required, but you'll probably also need to +configure :setting:`STATICFILES_URL` as well. + +.. setting:: STATICFILES_ROOT + +STATICFILES_ROOT +---------------- + +Default: ``''`` (Empty string) + +The absolute path to the directory that holds static files:: + + STATICFILES_ROOT = "/home/example.com/static/" + +This is a **required setting** unless you've overridden +:setting:`STATICFILES_STORAGE` and are using a custom storage backend. + +.. setting:: STATICFILES_URL + +STATICFILES_URL +--------------- + +Default: ``'/static/'`` + +The URL that handles the files served from :setting:`STATICFILES_ROOT`, e.g.:: + + STATICFILES_URL = '/site_media/static/' + +... or perhaps:: + + STATICFILES_URL = 'http://media.exmaple.com/' + +This should **always** have a trailing slash. + +.. setting:: STATICFILES_DIRS + +STATICFILES_DIRS +---------------- + +Default: ``[]`` + +This setting defines the additional locations the staticfiles app will traverse +if the :class:`FileSystemFinder` finder is enabled, e.g. if you use the +:djadmin:`collectstatic` or :djadmin:`findstatic` management command or use the +static file serving view. + +It should be defined as a sequence of ``(prefix, path)`` tuples, e.g.:: + + STATICFILES_DIRS = ( + ('', '/home/special.polls.com/polls/media'), + ('', '/home/polls.com/polls/media'), + ('common', '/opt/webfiles/common'), + ) + +.. setting:: STATICFILES_STORAGE + +STATICFILES_STORAGE +------------------- + +Default: ``'django.contrib.staticfiles.storage.StaticFilesStorage'`` + +The file storage engine to use when collecting static files with the +:djadmin:`collectstatic` management command. + +For an example, see :ref:`staticfiles-from-cdn`. + +.. setting:: STATICFILES_FINDERS + +STATICFILES_FINDERS +------------------- + +Default:: + + ("django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder") + +The list of finder backends that know how to find static files in +various locations. + +The default will find files stored in the :setting:`STATICFILES_DIRS` setting +(using :class:`django.contrib.staticfiles.finders.FileSystemFinder`) and in a +``static`` subdirectory of each app (using +:class:`django.contrib.staticfiles.finders.AppDirectoriesFinder`) + +One finder is disabled by default: +:class:`django.contrib.staticfiles.finders.DefaultStorageFinder`. If added to +your :setting:`STATICFILES_FINDERS` setting, it will look for static files in +the default file storage as defined by the :setting:`DEFAULT_FILE_STORAGE` +setting. + +.. note:: + + When using the :class:AppDirectoriesFinder` finder, make sure your apps can + be found by Django's app loading mechanism. Simply include a ``models`` + module (an empty ``models.py`` file suffices) and add the app to the + :setting:`INSTALLED_APPS` setting of your site. + +Static file finders are currently considered a private interface, and this +interface is thus undocumented. + +Management Commands +=================== + +.. highlight:: console + +``django.contrib.staticfiles`` exposes two management commands. + +collectstatic +------------- + +.. django-admin:: collectstatic + +Collects the static files into :setting:`STATICFILES_ROOT`. + +Duplicate file names are resolved in a similar way to how template resolution +works: files from apps later in :setting:`INSTALLED_APPS` overwrite those from +earlier apps, and files from storage directories later in +:setting:`STATICFILES_DIRS` overwrite those from earlier. If you're confused, +the :djadmin:`findstatic` command can help show you where + +Files are searched by using the :ref:`enabled finders +`. The default is to look in all locations defined in +:ref:`staticfiles-dirs` and in the ``media`` directory of apps specified by the +:setting:`INSTALLED_APPS` setting. + +Some commonly used options are: + +``--noinput`` + Do NOT prompt the user for input of any kind. + +``-i PATTERN`` or ``--ignore=PATTERN`` + Ignore files or directories matching this glob-style pattern. Use multiple + times to ignore more. + +``-n`` or ``--dry-run`` + Do everything except modify the filesystem. + +``-l`` or ``--link`` + Create a symbolic link to each file instead of copying. + +``--no-default-ignore`` + Don't ignore the common private glob-style patterns ``'CVS'``, ``'.*'`` + and ``'*~'``. + +For a full list of options, refer to the commands own help by running:: + + $ python manage.py collectstatic --help + +findstatic +---------- + +.. django-admin:: findstatic + +Searches for one or more relative paths with the enabled finders. + +For example:: + + $ python manage.py findstatic css/base.css admin/js/core.js + /home/special.polls.com/core/media/css/base.css + /home/polls.com/core/media/css/base.css + /home/polls.com/src/django/contrib/admin/media/js/core.js + +By default, all matching locations are found. To only return the first match +for each relative path, use the ``--first`` option:: + + $ python manage.py findstatic css/base.css --first + /home/special.polls.com/core/media/css/base.css + +This is a debugging aid; it'll show you exactly which static file will be +collected for a given path. + +Other Helpers +============= + +The ``media`` context processor +------------------------------- + +.. function:: django.contrib.staticfiles.context_processors.staticfiles + +This context processor adds the :setting:`STATICFILES_URL` into each template +context as the variable ``{{ STATICFILES_URL }}``. To use it, make sure that +``'django.contrib.staticfiles.context_processors.staticfiles'`` appears +somewhere in your :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting. + +Remember, only templates rendered with :class:`~django.template.RequestContext` +will have acces to the data provided by this (and any) context processor. + +.. templatetag:: get_staticfiles_prefix + +The ``get_staticfiles_prefix`` templatetag +========================================== + +.. highlight:: html+django + +If you're not using :class:`~django.template.RequestContext`, or if you need +more control over exactly where and how :setting:`STATICFILES_URL` is injected +into the template, you can use the :ttag:`get_staticfiles_prefix` template tag +instead:: + + {% load staticfiles %} + + +There's also a second form you can use to avoid extra processing if you need the +value multiple times:: + + {% load staticfiles %} + {% get_staticfiles_prefix as STATIC_PREFIX %} + + + + +.. _staticfiles-development-view: + +Static file development view +---------------------------- + +.. highlight:: python + +.. function:: django.contrib.staticfiles.views.serve(request, path) + +This view function serves static media in in development. + +.. warning:: + + This view will only work if :setting:`DEBUG` is ``True``. + + That's because this view is **grossly inefficient** and probably + **insecure**. This is only intended for local development, and should + **never be used in production**. + +To use the view, add the following snippet to the end of your primary URL +configuration:: + + from django.conf import settings + + if settings.DEBUG: + urlpatterns = patterns('django.contrib.staticfiles.views', + url(r'^static/(?P.*)$', 'serve'), + ) + +Note, the begin of the pattern (``r'^static/'``) should be your +:setting:`STATICFILES_URL` setting. + +Since this is a bit finicky, there's also a helper function that'll do this for you: + +.. function:: django.contrib.staticfiles.urls.staticfiles_urlpatterns() + +This will return the proper URL pattern for serving static files to your +already defined pattern list. Use it like this:: + + from django.contrib.staticfiles.urls import staticfiles_urlpatterns + + # ... the rest of your URLconf here ... + + urlpatterns += staticfiles_urlpatterns() + + diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 6ad5af9301..f0b4c562da 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1482,7 +1482,7 @@ Default:: ("django.contrib.auth.context_processors.auth", "django.core.context_processors.debug", "django.core.context_processors.i18n", - "django.core.context_processors.media", + "django.contrib.staticfiles.context_processors.staticfiles", "django.contrib.messages.context_processors.messages") A tuple of callables that are used to populate the context in ``RequestContext``. diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index 2ac4e653c4..4589084ac2 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -289,6 +289,8 @@ you'll see below. Subclassing Context: RequestContext ----------------------------------- +.. class:: django.template.RequestContext + Django comes with a special ``Context`` class, ``django.template.RequestContext``, that acts slightly differently than the normal ``django.template.Context``. The first difference is that it takes an @@ -309,7 +311,7 @@ and return a dictionary of items to be merged into the context. By default, ("django.contrib.auth.context_processors.auth", "django.core.context_processors.debug", "django.core.context_processors.i18n", - "django.core.context_processors.media", + "django.contrib.staticfiles.context_processors.staticfiles", "django.contrib.messages.context_processors.messages") .. versionadded:: 1.2 @@ -432,6 +434,11 @@ If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every ``RequestContext`` will contain a variable ``MEDIA_URL``, providing the value of the :setting:`MEDIA_URL` setting. +.. versionchanged:: 1.3 + This context processor has been moved to the new :ref:`staticfiles` app. + Please use the new ``django.contrib.staticfiles.context_processors.staticfiles`` + context processor. + django.core.context_processors.csrf ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/servers/tests.py b/tests/regressiontests/servers/tests.py index 47639828b0..29f169ccbd 100644 --- a/tests/regressiontests/servers/tests.py +++ b/tests/regressiontests/servers/tests.py @@ -9,6 +9,7 @@ from django.test import TestCase from django.core.handlers.wsgi import WSGIHandler from django.core.servers.basehttp import AdminMediaHandler +from django.conf import settings class AdminMediaHandlerTests(TestCase): @@ -25,7 +26,7 @@ class AdminMediaHandlerTests(TestCase): """ # Cases that should work on all platforms. data = ( - ('/media/css/base.css', ('css', 'base.css')), + ('%scss/base.css' % settings.ADMIN_MEDIA_PREFIX, ('css', 'base.css')), ) # Cases that should raise an exception. bad_data = () @@ -34,19 +35,19 @@ class AdminMediaHandlerTests(TestCase): if os.sep == '/': data += ( # URL, tuple of relative path parts. - ('/media/\\css/base.css', ('\\css', 'base.css')), + ('%s\\css/base.css' % settings.ADMIN_MEDIA_PREFIX, ('\\css', 'base.css')), ) bad_data += ( - '/media//css/base.css', - '/media////css/base.css', - '/media/../css/base.css', + '%s/css/base.css' % settings.ADMIN_MEDIA_PREFIX, + '%s///css/base.css' % settings.ADMIN_MEDIA_PREFIX, + '%s../css/base.css' % settings.ADMIN_MEDIA_PREFIX, ) elif os.sep == '\\': bad_data += ( - '/media/C:\css/base.css', - '/media//\\css/base.css', - '/media/\\css/base.css', - '/media/\\\\css/base.css' + '%sC:\css/base.css' % settings.ADMIN_MEDIA_PREFIX, + '%s/\\css/base.css' % settings.ADMIN_MEDIA_PREFIX, + '%s\\css/base.css' % settings.ADMIN_MEDIA_PREFIX, + '%s\\\\css/base.css' % settings.ADMIN_MEDIA_PREFIX ) for url, path_tuple in data: try: diff --git a/tests/regressiontests/staticfiles_tests/__init__.py b/tests/regressiontests/staticfiles_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/staticfiles_tests/apps/__init__.py b/tests/regressiontests/staticfiles_tests/apps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/staticfiles_tests/apps/no_label/__init__.py b/tests/regressiontests/staticfiles_tests/apps/no_label/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/staticfiles_tests/apps/no_label/models.py b/tests/regressiontests/staticfiles_tests/apps/no_label/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/staticfiles_tests/apps/no_label/static/file2.txt b/tests/regressiontests/staticfiles_tests/apps/no_label/static/file2.txt new file mode 100644 index 0000000000..aa264cab9b --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/apps/no_label/static/file2.txt @@ -0,0 +1 @@ +file2 in no_label_app diff --git a/tests/regressiontests/staticfiles_tests/apps/test/__init__.py b/tests/regressiontests/staticfiles_tests/apps/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/staticfiles_tests/apps/test/models.py b/tests/regressiontests/staticfiles_tests/apps/test/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/staticfiles_tests/apps/test/otherdir/odfile.txt b/tests/regressiontests/staticfiles_tests/apps/test/otherdir/odfile.txt new file mode 100644 index 0000000000..c62c93d162 --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/apps/test/otherdir/odfile.txt @@ -0,0 +1 @@ +File in otherdir. diff --git a/tests/regressiontests/staticfiles_tests/apps/test/static/test/.hidden b/tests/regressiontests/staticfiles_tests/apps/test/static/test/.hidden new file mode 100644 index 0000000000..cef6c23575 --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/apps/test/static/test/.hidden @@ -0,0 +1 @@ +This file should be ignored. diff --git a/tests/regressiontests/staticfiles_tests/apps/test/static/test/CVS b/tests/regressiontests/staticfiles_tests/apps/test/static/test/CVS new file mode 100644 index 0000000000..cef6c23575 --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/apps/test/static/test/CVS @@ -0,0 +1 @@ +This file should be ignored. diff --git a/tests/regressiontests/staticfiles_tests/apps/test/static/test/backup~ b/tests/regressiontests/staticfiles_tests/apps/test/static/test/backup~ new file mode 100644 index 0000000000..cef6c23575 --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/apps/test/static/test/backup~ @@ -0,0 +1 @@ +This file should be ignored. diff --git a/tests/regressiontests/staticfiles_tests/apps/test/static/test/file.txt b/tests/regressiontests/staticfiles_tests/apps/test/static/test/file.txt new file mode 100644 index 0000000000..169a206488 --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/apps/test/static/test/file.txt @@ -0,0 +1 @@ +In app media directory. diff --git a/tests/regressiontests/staticfiles_tests/apps/test/static/test/file1.txt b/tests/regressiontests/staticfiles_tests/apps/test/static/test/file1.txt new file mode 100644 index 0000000000..9f9a8d92ab --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/apps/test/static/test/file1.txt @@ -0,0 +1 @@ +file1 in the app dir \ No newline at end of file diff --git a/tests/regressiontests/staticfiles_tests/apps/test/static/test/test.ignoreme b/tests/regressiontests/staticfiles_tests/apps/test/static/test/test.ignoreme new file mode 100644 index 0000000000..d7df09c191 --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/apps/test/static/test/test.ignoreme @@ -0,0 +1 @@ +This file should be ignored. \ No newline at end of file diff --git a/tests/regressiontests/staticfiles_tests/models.py b/tests/regressiontests/staticfiles_tests/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/staticfiles_tests/project/documents/subdir/test.txt b/tests/regressiontests/staticfiles_tests/project/documents/subdir/test.txt new file mode 100644 index 0000000000..04326a212d --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/project/documents/subdir/test.txt @@ -0,0 +1 @@ +Can we find this file? diff --git a/tests/regressiontests/staticfiles_tests/project/documents/test.txt b/tests/regressiontests/staticfiles_tests/project/documents/test.txt new file mode 100644 index 0000000000..04326a212d --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/project/documents/test.txt @@ -0,0 +1 @@ +Can we find this file? diff --git a/tests/regressiontests/staticfiles_tests/project/documents/test/file.txt b/tests/regressiontests/staticfiles_tests/project/documents/test/file.txt new file mode 100644 index 0000000000..fdeaa23254 --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/project/documents/test/file.txt @@ -0,0 +1,2 @@ +In STATICFILES_DIRS directory. + diff --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 new file mode 100644 index 0000000000..466922d07a --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/project/site_media/media/media-file.txt @@ -0,0 +1 @@ +Media file. diff --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 new file mode 100644 index 0000000000..2eda9ce727 --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/project/site_media/static/test/storage.txt @@ -0,0 +1 @@ +Yeah! \ No newline at end of file diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py new file mode 100644 index 0000000000..2f39354627 --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/tests.py @@ -0,0 +1,330 @@ +import tempfile +import shutil +import os +import sys +import posixpath + +from django.test import TestCase +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.management import call_command +from django.db.models.loading import load_app +from django.template import Template, Context + +from django.contrib.staticfiles import finders, storage + +TEST_ROOT = os.path.dirname(__file__) + + +class StaticFilesTestCase(TestCase): + """ + Test case with a couple utility assertions. + """ + def setUp(self): + self.old_staticfiles_url = settings.STATICFILES_URL + self.old_staticfiles_root = settings.STATICFILES_ROOT + self.old_staticfiles_dirs = settings.STATICFILES_DIRS + self.old_staticfiles_finders = settings.STATICFILES_FINDERS + self.old_installed_apps = settings.INSTALLED_APPS + self.old_media_root = settings.MEDIA_ROOT + self.old_media_url = settings.MEDIA_URL + self.old_admin_media_prefix = settings.ADMIN_MEDIA_PREFIX + + # We have to load these apps to test staticfiles. + load_app('regressiontests.staticfiles_tests.apps.test') + load_app('regressiontests.staticfiles_tests.apps.no_label') + site_media = os.path.join(TEST_ROOT, 'project', 'site_media') + settings.MEDIA_ROOT = os.path.join(site_media, 'media') + settings.MEDIA_URL = '/media/' + settings.STATICFILES_ROOT = os.path.join(site_media, 'static') + settings.STATICFILES_URL = '/static/' + settings.ADMIN_MEDIA_PREFIX = '/static/admin/' + settings.STATICFILES_DIRS = ( + os.path.join(TEST_ROOT, 'project', 'documents'), + ) + settings.STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'django.contrib.staticfiles.finders.DefaultStorageFinder', + ) + + def tearDown(self): + settings.MEDIA_ROOT = self.old_media_root + settings.MEDIA_URL = self.old_media_url + settings.ADMIN_MEDIA_PREFIX = self.old_admin_media_prefix + settings.STATICFILES_ROOT = self.old_staticfiles_root + settings.STATICFILES_URL = self.old_staticfiles_url + settings.STATICFILES_DIRS = self.old_staticfiles_dirs + settings.STATICFILES_FINDERS = self.old_staticfiles_finders + settings.INSTALLED_APPS = self.old_installed_apps + + def assertFileContains(self, filepath, text): + self.failUnless(text in self._get_file(filepath), + "'%s' not in '%s'" % (text, filepath)) + + def assertFileNotFound(self, filepath): + self.assertRaises(IOError, self._get_file, filepath) + + +class BuildStaticTestCase(StaticFilesTestCase): + """ + Tests shared by all file-resolving features (collectstatic, + findstatic, and static serve view). + + This relies on the asserts defined in UtilityAssertsTestCase, but + is separated because some test cases need those asserts without + all these tests. + """ + def setUp(self): + super(BuildStaticTestCase, self).setUp() + self.old_staticfiles_storage = settings.STATICFILES_STORAGE + self.old_root = settings.STATICFILES_ROOT + settings.STATICFILES_ROOT = tempfile.mkdtemp() + self.run_collectstatic() + + def tearDown(self): + shutil.rmtree(settings.STATICFILES_ROOT) + settings.STATICFILES_ROOT = self.old_root + super(BuildStaticTestCase, self).tearDown() + + def run_collectstatic(self, **kwargs): + call_command('collectstatic', interactive=False, verbosity='0', + ignore_patterns=['*.ignoreme'], **kwargs) + + def _get_file(self, filepath): + assert filepath, 'filepath is empty.' + filepath = os.path.join(settings.STATICFILES_ROOT, filepath) + return open(filepath).read() + + +class TestDefaults(object): + """ + A few standard test cases. + """ + def test_staticfiles_dirs(self): + """ + Can find a file in a STATICFILES_DIRS directory. + + """ + self.assertFileContains('test.txt', 'Can we find') + + def test_staticfiles_dirs_subdir(self): + """ + Can find a file in a subdirectory of a STATICFILES_DIRS + directory. + + """ + self.assertFileContains('subdir/test.txt', 'Can we find') + + def test_staticfiles_dirs_priority(self): + """ + File in STATICFILES_DIRS has priority over file in app. + + """ + self.assertFileContains('test/file.txt', 'STATICFILES_DIRS') + + def test_app_files(self): + """ + Can find a file in an app media/ directory. + + """ + self.assertFileContains('test/file1.txt', 'file1 in the app dir') + + +class TestBuildStatic(BuildStaticTestCase, TestDefaults): + """ + Test ``collectstatic`` management command. + """ + def test_ignore(self): + """ + Test that -i patterns are ignored. + """ + self.assertFileNotFound('test/test.ignoreme') + + def test_common_ignore_patterns(self): + """ + Common ignore patterns (*~, .*, CVS) are ignored. + """ + self.assertFileNotFound('test/.hidden') + self.assertFileNotFound('test/backup~') + self.assertFileNotFound('test/CVS') + + +class TestBuildStaticExcludeNoDefaultIgnore(BuildStaticTestCase, TestDefaults): + """ + Test ``--exclude-dirs`` and ``--no-default-ignore`` options for + ``collectstatic`` management command. + """ + def run_collectstatic(self): + super(TestBuildStaticExcludeNoDefaultIgnore, self).run_collectstatic( + use_default_ignore_patterns=False) + + def test_no_common_ignore_patterns(self): + """ + With --no-default-ignore, common ignore patterns (*~, .*, CVS) + are not ignored. + + """ + self.assertFileContains('test/.hidden', 'should be ignored') + self.assertFileContains('test/backup~', 'should be ignored') + self.assertFileContains('test/CVS', 'should be ignored') + + +class TestBuildStaticDryRun(BuildStaticTestCase): + """ + Test ``--dry-run`` option for ``collectstatic`` management command. + """ + def run_collectstatic(self): + super(TestBuildStaticDryRun, self).run_collectstatic(dry_run=True) + + def test_no_files_created(self): + """ + With --dry-run, no files created in destination dir. + """ + self.assertEquals(os.listdir(settings.STATICFILES_ROOT), []) + + +if sys.platform != 'win32': + class TestBuildStaticLinks(BuildStaticTestCase, TestDefaults): + """ + Test ``--link`` option for ``collectstatic`` management command. + + Note that by inheriting ``TestDefaults`` we repeat all + the standard file resolving tests here, to make sure using + ``--link`` does not change the file-selection semantics. + """ + def run_collectstatic(self): + super(TestBuildStaticLinks, self).run_collectstatic(link=True) + + def test_links_created(self): + """ + With ``--link``, symbolic links are created. + + """ + self.failUnless(os.path.islink(os.path.join(settings.STATICFILES_ROOT, 'test.txt'))) + + +class TestServeStatic(StaticFilesTestCase): + """ + Test static asset serving view. + """ + def _response(self, filepath): + return self.client.get( + posixpath.join(settings.STATICFILES_URL, filepath)) + + def assertFileContains(self, filepath, text): + self.assertContains(self._response(filepath), text) + + def assertFileNotFound(self, filepath): + self.assertEquals(self._response(filepath).status_code, 404) + + +class TestServeStaticWithDefaultURL(TestServeStatic, TestDefaults): + """ + Test static asset serving view with staticfiles_urlpatterns helper. + """ + urls = "regressiontests.staticfiles_tests.urls.default" + + +class TestServeStaticWithURLHelper(TestServeStatic, TestDefaults): + """ + Test static asset serving view with staticfiles_urlpatterns helper. + """ + urls = "regressiontests.staticfiles_tests.urls.helper" + + +class TestServeAdminMedia(TestServeStatic): + """ + Test serving media from django.contrib.admin. + """ + def _response(self, filepath): + return self.client.get( + posixpath.join(settings.ADMIN_MEDIA_PREFIX, filepath)) + + def test_serve_admin_media(self): + self.assertFileContains('css/base.css', 'body') + + +class FinderTestCase(object): + """ + Base finder test mixin + """ + def test_find_first(self): + src, dst = self.find_first + self.assertEquals(self.finder.find(src), dst) + + def test_find_all(self): + src, dst = self.find_all + self.assertEquals(self.finder.find(src, all=True), dst) + + +class TestFileSystemFinder(StaticFilesTestCase, FinderTestCase): + """ + Test FileSystemFinder. + """ + def setUp(self): + super(TestFileSystemFinder, self).setUp() + self.finder = finders.FileSystemFinder() + test_file_path = os.path.join(TEST_ROOT, 'project/documents/test/file.txt') + self.find_first = ("test/file.txt", test_file_path) + self.find_all = ("test/file.txt", [test_file_path]) + + +class TestAppDirectoriesFinder(StaticFilesTestCase, FinderTestCase): + """ + Test AppDirectoriesFinder. + """ + def setUp(self): + super(TestAppDirectoriesFinder, self).setUp() + self.finder = finders.AppDirectoriesFinder() + test_file_path = os.path.join(TEST_ROOT, 'apps/test/static/test/file1.txt') + self.find_first = ("test/file1.txt", test_file_path) + self.find_all = ("test/file1.txt", [test_file_path]) + + +class TestDefaultStorageFinder(StaticFilesTestCase, FinderTestCase): + """ + Test DefaultStorageFinder. + """ + def setUp(self): + super(TestDefaultStorageFinder, self).setUp() + self.finder = finders.DefaultStorageFinder( + storage=storage.StaticFilesStorage(location=settings.MEDIA_ROOT)) + test_file_path = os.path.join(settings.MEDIA_ROOT, 'media-file.txt') + self.find_first = ("media-file.txt", test_file_path) + self.find_all = ("media-file.txt", [test_file_path]) + + +class TestMiscFinder(TestCase): + """ + A few misc finder tests. + """ + def test_get_finder(self): + self.assertTrue(isinstance(finders.get_finder( + "django.contrib.staticfiles.finders.FileSystemFinder"), + finders.FileSystemFinder)) + self.assertRaises(ImproperlyConfigured, + finders.get_finder, "django.contrib.staticfiles.finders.FooBarFinder") + self.assertRaises(ImproperlyConfigured, + finders.get_finder, "foo.bar.FooBarFinder") + + +class TemplateTagTest(TestCase): + def test_get_staticfiles_prefix(self): + """ + Test the get_staticfiles_prefix helper return the STATICFILES_URL setting. + """ + self.assertEquals(Template( + "{% load staticfiles %}" + "{% get_staticfiles_prefix %}" + ).render(Context()), settings.STATICFILES_URL) + + def test_get_staticfiles_prefix_with_as(self): + """ + Test the get_staticfiles_prefix helper return the STATICFILES_URL setting. + """ + self.assertEquals(Template( + "{% load staticfiles %}" + "{% get_staticfiles_prefix as staticfiles_prefix %}" + "{{ staticfiles_prefix }}" + ).render(Context()), settings.STATICFILES_URL) diff --git a/tests/regressiontests/staticfiles_tests/urls/__init__.py b/tests/regressiontests/staticfiles_tests/urls/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/staticfiles_tests/urls/default.py b/tests/regressiontests/staticfiles_tests/urls/default.py new file mode 100644 index 0000000000..061ec6465e --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/urls/default.py @@ -0,0 +1,6 @@ +from django.conf import settings +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + url(r'^static/(?P.*)$', 'django.contrib.staticfiles.views.serve'), +) diff --git a/tests/regressiontests/staticfiles_tests/urls/helper.py b/tests/regressiontests/staticfiles_tests/urls/helper.py new file mode 100644 index 0000000000..e4951d1afb --- /dev/null +++ b/tests/regressiontests/staticfiles_tests/urls/helper.py @@ -0,0 +1,3 @@ +from django.contrib.staticfiles.urls import staticfiles_urlpatterns + +urlpatterns = staticfiles_urlpatterns() diff --git a/tests/runtests.py b/tests/runtests.py index a5f7479128..055c910386 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -27,6 +27,7 @@ ALWAYS_INSTALLED_APPS = [ 'django.contrib.comments', 'django.contrib.admin', 'django.contrib.admindocs', + 'django.contrib.staticfiles', ] def get_test_models(): diff --git a/tests/urls.py b/tests/urls.py index 01d6408c5a..573a0aafb7 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -41,4 +41,7 @@ urlpatterns = patterns('', # special headers views (r'special_headers/', include('regressiontests.special_headers.urls')), + + # static files handling + (r'^', include('regressiontests.staticfiles_tests.urls.default')), )