测试gitnore
This commit is contained in:
@@ -5,9 +5,9 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class StaticFilesConfig(AppConfig):
|
||||
name = "django.contrib.staticfiles"
|
||||
name = 'django.contrib.staticfiles'
|
||||
verbose_name = _("Static Files")
|
||||
ignore_patterns = ["CVS", ".*", "*~"]
|
||||
ignore_patterns = ['CVS', '.*', '*~']
|
||||
|
||||
def ready(self):
|
||||
checks.register(check_finders, checks.Tags.staticfiles)
|
||||
|
||||
@@ -4,9 +4,11 @@ import os
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import utils
|
||||
from django.core.checks import Error, Warning
|
||||
from django.core.checks import Error
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.files.storage import FileSystemStorage, Storage, default_storage
|
||||
from django.core.files.storage import (
|
||||
FileSystemStorage, Storage, default_storage,
|
||||
)
|
||||
from django.utils._os import safe_join
|
||||
from django.utils.functional import LazyObject, empty
|
||||
from django.utils.module_loading import import_string
|
||||
@@ -19,11 +21,10 @@ class BaseFinder:
|
||||
"""
|
||||
A base file finder to be used for custom staticfiles finder classes.
|
||||
"""
|
||||
|
||||
def check(self, **kwargs):
|
||||
raise NotImplementedError(
|
||||
"subclasses may provide a check() method to verify the finder is "
|
||||
"configured correctly."
|
||||
'subclasses may provide a check() method to verify the finder is '
|
||||
'configured correctly.'
|
||||
)
|
||||
|
||||
def find(self, path, all=False):
|
||||
@@ -33,18 +34,14 @@ class BaseFinder:
|
||||
If the ``all`` parameter is False (default) return only the first found
|
||||
file path; if True, return a list of all found files paths.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseFinder must provide a find() method"
|
||||
)
|
||||
raise NotImplementedError('subclasses of BaseFinder must provide a find() method')
|
||||
|
||||
def list(self, ignore_patterns):
|
||||
"""
|
||||
Given an optional list of paths to ignore, return a two item iterable
|
||||
consisting of the relative path and storage instance.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseFinder must provide a list() method"
|
||||
)
|
||||
raise NotImplementedError('subclasses of BaseFinder must provide a list() method')
|
||||
|
||||
|
||||
class FileSystemFinder(BaseFinder):
|
||||
@@ -52,7 +49,6 @@ class FileSystemFinder(BaseFinder):
|
||||
A static files finder that uses the ``STATICFILES_DIRS`` setting
|
||||
to locate files.
|
||||
"""
|
||||
|
||||
def __init__(self, app_names=None, *args, **kwargs):
|
||||
# List of locations with static files
|
||||
self.locations = []
|
||||
@@ -62,7 +58,7 @@ class FileSystemFinder(BaseFinder):
|
||||
if isinstance(root, (list, tuple)):
|
||||
prefix, root = root
|
||||
else:
|
||||
prefix = ""
|
||||
prefix = ''
|
||||
if (prefix, root) not in self.locations:
|
||||
self.locations.append((prefix, root))
|
||||
for prefix, root in self.locations:
|
||||
@@ -74,43 +70,26 @@ class FileSystemFinder(BaseFinder):
|
||||
def check(self, **kwargs):
|
||||
errors = []
|
||||
if not isinstance(settings.STATICFILES_DIRS, (list, tuple)):
|
||||
errors.append(
|
||||
Error(
|
||||
"The STATICFILES_DIRS setting is not a tuple or list.",
|
||||
hint="Perhaps you forgot a trailing comma?",
|
||||
id="staticfiles.E001",
|
||||
)
|
||||
)
|
||||
return errors
|
||||
errors.append(Error(
|
||||
'The STATICFILES_DIRS setting is not a tuple or list.',
|
||||
hint='Perhaps you forgot a trailing comma?',
|
||||
id='staticfiles.E001',
|
||||
))
|
||||
for root in settings.STATICFILES_DIRS:
|
||||
if isinstance(root, (list, tuple)):
|
||||
prefix, root = root
|
||||
if prefix.endswith("/"):
|
||||
errors.append(
|
||||
Error(
|
||||
"The prefix %r in the STATICFILES_DIRS setting must "
|
||||
"not end with a slash." % prefix,
|
||||
id="staticfiles.E003",
|
||||
)
|
||||
)
|
||||
if settings.STATIC_ROOT and os.path.abspath(
|
||||
settings.STATIC_ROOT
|
||||
) == os.path.abspath(root):
|
||||
errors.append(
|
||||
Error(
|
||||
"The STATICFILES_DIRS setting should not contain the "
|
||||
"STATIC_ROOT setting.",
|
||||
id="staticfiles.E002",
|
||||
)
|
||||
)
|
||||
if not os.path.isdir(root):
|
||||
errors.append(
|
||||
Warning(
|
||||
f"The directory '{root}' in the STATICFILES_DIRS setting "
|
||||
f"does not exist.",
|
||||
id="staticfiles.W004",
|
||||
)
|
||||
)
|
||||
if prefix.endswith('/'):
|
||||
errors.append(Error(
|
||||
'The prefix %r in the STATICFILES_DIRS setting must '
|
||||
'not end with a slash.' % prefix,
|
||||
id='staticfiles.E003',
|
||||
))
|
||||
if settings.STATIC_ROOT and os.path.abspath(settings.STATIC_ROOT) == os.path.abspath(root):
|
||||
errors.append(Error(
|
||||
'The STATICFILES_DIRS setting should not contain the '
|
||||
'STATIC_ROOT setting.',
|
||||
id='staticfiles.E002',
|
||||
))
|
||||
return errors
|
||||
|
||||
def find(self, path, all=False):
|
||||
@@ -134,10 +113,10 @@ class FileSystemFinder(BaseFinder):
|
||||
absolute path (or ``None`` if no match).
|
||||
"""
|
||||
if prefix:
|
||||
prefix = "%s%s" % (prefix, os.sep)
|
||||
prefix = '%s%s' % (prefix, os.sep)
|
||||
if not path.startswith(prefix):
|
||||
return None
|
||||
path = path[len(prefix) :]
|
||||
path = path[len(prefix):]
|
||||
path = safe_join(root, path)
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
@@ -147,11 +126,9 @@ class FileSystemFinder(BaseFinder):
|
||||
List all files in all locations.
|
||||
"""
|
||||
for prefix, root in self.locations:
|
||||
# Skip nonexistent directories.
|
||||
if os.path.isdir(root):
|
||||
storage = self.storages[root]
|
||||
for path in utils.get_files(storage, ignore_patterns):
|
||||
yield path, storage
|
||||
storage = self.storages[root]
|
||||
for path in utils.get_files(storage, ignore_patterns):
|
||||
yield path, storage
|
||||
|
||||
|
||||
class AppDirectoriesFinder(BaseFinder):
|
||||
@@ -159,9 +136,8 @@ class AppDirectoriesFinder(BaseFinder):
|
||||
A static files finder that looks in the directory of each app as
|
||||
specified in the source_dir attribute.
|
||||
"""
|
||||
|
||||
storage_class = FileSystemStorage
|
||||
source_dir = "static"
|
||||
source_dir = 'static'
|
||||
|
||||
def __init__(self, app_names=None, *args, **kwargs):
|
||||
# The list of apps that are handled
|
||||
@@ -174,8 +150,7 @@ class AppDirectoriesFinder(BaseFinder):
|
||||
app_configs = [ac for ac in app_configs if ac.name in app_names]
|
||||
for app_config in app_configs:
|
||||
app_storage = self.storage_class(
|
||||
os.path.join(app_config.path, self.source_dir)
|
||||
)
|
||||
os.path.join(app_config.path, self.source_dir))
|
||||
if os.path.isdir(app_storage.location):
|
||||
self.storages[app_config.name] = app_storage
|
||||
if app_config.name not in self.apps:
|
||||
@@ -187,7 +162,7 @@ class AppDirectoriesFinder(BaseFinder):
|
||||
List all files in all app storages.
|
||||
"""
|
||||
for storage in self.storages.values():
|
||||
if storage.exists(""): # check if storage location exists
|
||||
if storage.exists(''): # check if storage location exists
|
||||
for path in utils.get_files(storage, ignore_patterns):
|
||||
yield path, storage
|
||||
|
||||
@@ -224,18 +199,15 @@ 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__
|
||||
)
|
||||
raise ImproperlyConfigured("The staticfiles storage finder %r "
|
||||
"doesn't have a storage class "
|
||||
"assigned." % self.__class__)
|
||||
# Make sure we have a storage instance here.
|
||||
if not isinstance(self.storage, (Storage, LazyObject)):
|
||||
self.storage = self.storage()
|
||||
@@ -246,7 +218,7 @@ class BaseStorageFinder(BaseFinder):
|
||||
Look for files in the default file storage, if it's local.
|
||||
"""
|
||||
try:
|
||||
self.storage.path("")
|
||||
self.storage.path('')
|
||||
except NotImplementedError:
|
||||
pass
|
||||
else:
|
||||
@@ -271,18 +243,15 @@ class DefaultStorageFinder(BaseStorageFinder):
|
||||
"""
|
||||
A static files finder that uses the default storage backend.
|
||||
"""
|
||||
|
||||
storage = default_storage
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
base_location = getattr(self.storage, "base_location", empty)
|
||||
base_location = getattr(self.storage, 'base_location', empty)
|
||||
if not base_location:
|
||||
raise ImproperlyConfigured(
|
||||
"The storage backend of the "
|
||||
"staticfiles finder %r doesn't have "
|
||||
"a valid location." % self.__class__
|
||||
)
|
||||
raise ImproperlyConfigured("The storage backend of the "
|
||||
"staticfiles finder %r doesn't have "
|
||||
"a valid location." % self.__class__)
|
||||
|
||||
|
||||
def find(path, all=False):
|
||||
@@ -320,7 +289,6 @@ def get_finder(import_path):
|
||||
"""
|
||||
Finder = import_string(import_path)
|
||||
if not issubclass(Finder, BaseFinder):
|
||||
raise ImproperlyConfigured(
|
||||
'Finder "%s" is not a subclass of "%s"' % (Finder, BaseFinder)
|
||||
)
|
||||
raise ImproperlyConfigured('Finder "%s" is not a subclass of "%s"' %
|
||||
(Finder, BaseFinder))
|
||||
return Finder()
|
||||
|
||||
@@ -16,7 +16,6 @@ class StaticFilesHandlerMixin:
|
||||
"""
|
||||
Common methods used by WSGI and ASGI handlers.
|
||||
"""
|
||||
|
||||
# May be used to differentiate between handler types (e.g. in a
|
||||
# request_finished signal)
|
||||
handles_files = True
|
||||
@@ -42,7 +41,7 @@ class StaticFilesHandlerMixin:
|
||||
"""
|
||||
Return the relative path to the media file on disk for the given URL.
|
||||
"""
|
||||
relative_url = url[len(self.base_url[2]) :]
|
||||
relative_url = url[len(self.base_url[2]):]
|
||||
return url2pathname(relative_url)
|
||||
|
||||
def serve(self, request):
|
||||
@@ -59,9 +58,7 @@ class StaticFilesHandlerMixin:
|
||||
try:
|
||||
return await sync_to_async(self.serve, thread_sensitive=False)(request)
|
||||
except Http404 as e:
|
||||
return await sync_to_async(response_for_exception, thread_sensitive=False)(
|
||||
request, e
|
||||
)
|
||||
return await sync_to_async(response_for_exception, thread_sensitive=False)(request, e)
|
||||
|
||||
|
||||
class StaticFilesHandler(StaticFilesHandlerMixin, WSGIHandler):
|
||||
@@ -69,7 +66,6 @@ class StaticFilesHandler(StaticFilesHandlerMixin, WSGIHandler):
|
||||
WSGI middleware that intercepts calls to the static files directory, as
|
||||
defined by the STATIC_URL setting, and serves those files.
|
||||
"""
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
self.base_url = urlparse(self.get_base_url())
|
||||
@@ -86,14 +82,13 @@ class ASGIStaticFilesHandler(StaticFilesHandlerMixin, ASGIHandler):
|
||||
ASGI application which wraps another and intercepts requests for static
|
||||
files, passing them off to Django's static file serving.
|
||||
"""
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
self.base_url = urlparse(self.get_base_url())
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
# Only even look at HTTP requests
|
||||
if scope["type"] == "http" and self._should_handle(scope["path"]):
|
||||
if scope['type'] == 'http' and self._should_handle(scope['path']):
|
||||
# Serve static content
|
||||
# (the one thing super() doesn't do is __call__, apparently)
|
||||
return await super().__call__(scope, receive, send)
|
||||
|
||||
+66
-96
@@ -15,7 +15,6 @@ class Command(BaseCommand):
|
||||
Copies or symlinks static files from different locations to the
|
||||
settings.STATIC_ROOT.
|
||||
"""
|
||||
|
||||
help = "Collect static files in a single location."
|
||||
requires_system_checks = [Tags.staticfiles]
|
||||
|
||||
@@ -31,78 +30,58 @@ class Command(BaseCommand):
|
||||
@cached_property
|
||||
def local(self):
|
||||
try:
|
||||
self.storage.path("")
|
||||
self.storage.path('')
|
||||
except NotImplementedError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--noinput",
|
||||
"--no-input",
|
||||
action="store_false",
|
||||
dest="interactive",
|
||||
'--noinput', '--no-input', action='store_false', dest='interactive',
|
||||
help="Do NOT prompt the user for input of any kind.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-post-process",
|
||||
action="store_false",
|
||||
dest="post_process",
|
||||
'--no-post-process', action='store_false', dest='post_process',
|
||||
help="Do NOT post process collected files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--ignore",
|
||||
action="append",
|
||||
default=[],
|
||||
dest="ignore_patterns",
|
||||
metavar="PATTERN",
|
||||
'-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.",
|
||||
"pattern. Use multiple times to ignore more.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
'-n', '--dry-run', action='store_true',
|
||||
help="Do everything except modify the filesystem.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--clear",
|
||||
action="store_true",
|
||||
'-c', '--clear', action='store_true',
|
||||
help="Clear the existing files using the storage "
|
||||
"before trying to copy or link the original file.",
|
||||
"before trying to copy or link the original file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--link",
|
||||
action="store_true",
|
||||
'-l', '--link', action='store_true',
|
||||
help="Create a symbolic link to each file instead of copying.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-default-ignore",
|
||||
action="store_false",
|
||||
dest="use_default_ignore_patterns",
|
||||
help=(
|
||||
"Don't ignore the common private glob-style patterns (defaults to "
|
||||
"'CVS', '.*' and '*~')."
|
||||
),
|
||||
'--no-default-ignore', action='store_false', dest='use_default_ignore_patterns',
|
||||
help="Don't ignore the common private glob-style patterns (defaults to 'CVS', '.*' and '*~').",
|
||||
)
|
||||
|
||||
def set_options(self, **options):
|
||||
"""
|
||||
Set instance variables based on an options dict
|
||||
"""
|
||||
self.interactive = options["interactive"]
|
||||
self.verbosity = options["verbosity"]
|
||||
self.symlink = options["link"]
|
||||
self.clear = options["clear"]
|
||||
self.dry_run = options["dry_run"]
|
||||
ignore_patterns = options["ignore_patterns"]
|
||||
if options["use_default_ignore_patterns"]:
|
||||
ignore_patterns += apps.get_app_config("staticfiles").ignore_patterns
|
||||
self.interactive = options['interactive']
|
||||
self.verbosity = options['verbosity']
|
||||
self.symlink = options['link']
|
||||
self.clear = options['clear']
|
||||
self.dry_run = options['dry_run']
|
||||
ignore_patterns = options['ignore_patterns']
|
||||
if options['use_default_ignore_patterns']:
|
||||
ignore_patterns += apps.get_app_config('staticfiles').ignore_patterns
|
||||
self.ignore_patterns = list({os.path.normpath(p) for p in ignore_patterns})
|
||||
self.post_process = options["post_process"]
|
||||
self.post_process = options['post_process']
|
||||
|
||||
def collect(self):
|
||||
"""
|
||||
@@ -114,7 +93,7 @@ class Command(BaseCommand):
|
||||
raise CommandError("Can't symlink to a remote destination.")
|
||||
|
||||
if self.clear:
|
||||
self.clear_dir("")
|
||||
self.clear_dir('')
|
||||
|
||||
if self.symlink:
|
||||
handler = self.link_file
|
||||
@@ -125,7 +104,7 @@ class Command(BaseCommand):
|
||||
for finder in get_finders():
|
||||
for path, storage in finder.list(self.ignore_patterns):
|
||||
# Prefix the relative path if the source storage contains it
|
||||
if getattr(storage, "prefix", None):
|
||||
if getattr(storage, 'prefix', None):
|
||||
prefixed_path = os.path.join(storage.prefix, path)
|
||||
else:
|
||||
prefixed_path = path
|
||||
@@ -143,8 +122,9 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
# Storage backends may define a post_process() method.
|
||||
if self.post_process and hasattr(self.storage, "post_process"):
|
||||
processor = self.storage.post_process(found_files, dry_run=self.dry_run)
|
||||
if self.post_process and hasattr(self.storage, 'post_process'):
|
||||
processor = self.storage.post_process(found_files,
|
||||
dry_run=self.dry_run)
|
||||
for original_path, processed_path, processed in processor:
|
||||
if isinstance(processed, Exception):
|
||||
self.stderr.write("Post-processing '%s' failed!" % original_path)
|
||||
@@ -153,85 +133,75 @@ class Command(BaseCommand):
|
||||
self.stderr.write()
|
||||
raise processed
|
||||
if processed:
|
||||
self.log(
|
||||
"Post-processed '%s' as '%s'" % (original_path, processed_path),
|
||||
level=2,
|
||||
)
|
||||
self.log("Post-processed '%s' as '%s'" %
|
||||
(original_path, processed_path), level=2)
|
||||
self.post_processed_files.append(original_path)
|
||||
else:
|
||||
self.log("Skipped post-processing '%s'" % original_path)
|
||||
|
||||
return {
|
||||
"modified": self.copied_files + self.symlinked_files,
|
||||
"unmodified": self.unmodified_files,
|
||||
"post_processed": self.post_processed_files,
|
||||
'modified': self.copied_files + self.symlinked_files,
|
||||
'unmodified': self.unmodified_files,
|
||||
'post_processed': self.post_processed_files,
|
||||
}
|
||||
|
||||
def handle(self, **options):
|
||||
self.set_options(**options)
|
||||
message = ["\n"]
|
||||
message = ['\n']
|
||||
if self.dry_run:
|
||||
message.append(
|
||||
"You have activated the --dry-run option so no files will be "
|
||||
"modified.\n\n"
|
||||
'You have activated the --dry-run option so no files will be modified.\n\n'
|
||||
)
|
||||
|
||||
message.append(
|
||||
"You have requested to collect static files at the destination\n"
|
||||
"location as specified in your settings"
|
||||
'You have requested to collect static files at the destination\n'
|
||||
'location as specified in your settings'
|
||||
)
|
||||
|
||||
if self.is_local_storage() and self.storage.location:
|
||||
destination_path = self.storage.location
|
||||
message.append(":\n\n %s\n\n" % destination_path)
|
||||
should_warn_user = self.storage.exists(destination_path) and any(
|
||||
self.storage.listdir(destination_path)
|
||||
message.append(':\n\n %s\n\n' % destination_path)
|
||||
should_warn_user = (
|
||||
self.storage.exists(destination_path) and
|
||||
any(self.storage.listdir(destination_path))
|
||||
)
|
||||
else:
|
||||
destination_path = None
|
||||
message.append(".\n\n")
|
||||
message.append('.\n\n')
|
||||
# Destination files existence not checked; play it safe and warn.
|
||||
should_warn_user = True
|
||||
|
||||
if self.interactive and should_warn_user:
|
||||
if self.clear:
|
||||
message.append("This will DELETE ALL FILES in this location!\n")
|
||||
message.append('This will DELETE ALL FILES in this location!\n')
|
||||
else:
|
||||
message.append("This will overwrite existing files!\n")
|
||||
message.append('This will overwrite existing files!\n')
|
||||
|
||||
message.append(
|
||||
"Are you sure you want to do this?\n\n"
|
||||
'Are you sure you want to do this?\n\n'
|
||||
"Type 'yes' to continue, or 'no' to cancel: "
|
||||
)
|
||||
if input("".join(message)) != "yes":
|
||||
if input(''.join(message)) != 'yes':
|
||||
raise CommandError("Collecting static files cancelled.")
|
||||
|
||||
collected = self.collect()
|
||||
|
||||
if self.verbosity >= 1:
|
||||
modified_count = len(collected["modified"])
|
||||
unmodified_count = len(collected["unmodified"])
|
||||
post_processed_count = len(collected["post_processed"])
|
||||
modified_count = len(collected['modified'])
|
||||
unmodified_count = len(collected['unmodified'])
|
||||
post_processed_count = len(collected['post_processed'])
|
||||
return (
|
||||
"\n%(modified_count)s %(identifier)s %(action)s"
|
||||
"%(destination)s%(unmodified)s%(post_processed)s."
|
||||
) % {
|
||||
"modified_count": modified_count,
|
||||
"identifier": "static file" + ("" if modified_count == 1 else "s"),
|
||||
"action": "symlinked" if self.symlink else "copied",
|
||||
"destination": (
|
||||
" to '%s'" % destination_path if destination_path else ""
|
||||
),
|
||||
"unmodified": (
|
||||
", %s unmodified" % unmodified_count
|
||||
if collected["unmodified"]
|
||||
else ""
|
||||
),
|
||||
"post_processed": (
|
||||
collected["post_processed"]
|
||||
and ", %s post-processed" % post_processed_count
|
||||
or ""
|
||||
),
|
||||
'modified_count': modified_count,
|
||||
'identifier': 'static file' + ('' if modified_count == 1 else 's'),
|
||||
'action': 'symlinked' if self.symlink else 'copied',
|
||||
'destination': (" to '%s'" % destination_path if destination_path else ''),
|
||||
'unmodified': (', %s unmodified' % unmodified_count if collected['unmodified'] else ''),
|
||||
'post_processed': (collected['post_processed'] and
|
||||
', %s post-processed'
|
||||
% post_processed_count or ''),
|
||||
}
|
||||
|
||||
def log(self, msg, level=2):
|
||||
@@ -298,17 +268,16 @@ class Command(BaseCommand):
|
||||
# previous collectstatic was with --link), the old
|
||||
# links/files must be deleted so it's not safe to skip
|
||||
# unmodified files.
|
||||
can_skip_unmodified_files = not (
|
||||
self.symlink ^ os.path.islink(full_path)
|
||||
)
|
||||
can_skip_unmodified_files = not (self.symlink ^ os.path.islink(full_path))
|
||||
else:
|
||||
# In remote storages, skipping is only based on the
|
||||
# modified times since symlinks aren't relevant.
|
||||
can_skip_unmodified_files = True
|
||||
# Avoid sub-second precision (see #14665, #19540)
|
||||
file_is_unmodified = target_last_modified.replace(
|
||||
microsecond=0
|
||||
) >= source_last_modified.replace(microsecond=0)
|
||||
file_is_unmodified = (
|
||||
target_last_modified.replace(microsecond=0) >=
|
||||
source_last_modified.replace(microsecond=0)
|
||||
)
|
||||
if file_is_unmodified and can_skip_unmodified_files:
|
||||
if prefixed_path not in self.unmodified_files:
|
||||
self.unmodified_files.append(prefixed_path)
|
||||
@@ -345,13 +314,14 @@ class Command(BaseCommand):
|
||||
if os.path.lexists(full_path):
|
||||
os.unlink(full_path)
|
||||
os.symlink(source_path, full_path)
|
||||
except AttributeError:
|
||||
import platform
|
||||
raise CommandError("Symlinking is not supported by Python %s." %
|
||||
platform.python_version())
|
||||
except NotImplementedError:
|
||||
import platform
|
||||
|
||||
raise CommandError(
|
||||
"Symlinking is not supported in this "
|
||||
"platform (%s)." % platform.platform()
|
||||
)
|
||||
raise CommandError("Symlinking is not supported in this "
|
||||
"platform (%s)." % platform.platform())
|
||||
except OSError as e:
|
||||
raise CommandError(e)
|
||||
if prefixed_path not in self.symlinked_files:
|
||||
|
||||
+12
-17
@@ -6,43 +6,38 @@ from django.core.management.base import LabelCommand
|
||||
|
||||
class Command(LabelCommand):
|
||||
help = "Finds the absolute paths for the given static file(s)."
|
||||
label = "staticfile"
|
||||
label = 'staticfile'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--first",
|
||||
action="store_false",
|
||||
dest="all",
|
||||
'--first', action='store_false', dest='all',
|
||||
help="Only return the first match for each static file.",
|
||||
)
|
||||
|
||||
def handle_label(self, path, **options):
|
||||
verbosity = options["verbosity"]
|
||||
result = finders.find(path, all=options["all"])
|
||||
verbosity = options['verbosity']
|
||||
result = finders.find(path, all=options['all'])
|
||||
if verbosity >= 2:
|
||||
searched_locations = (
|
||||
"\nLooking in the following locations:\n %s"
|
||||
% "\n ".join([str(loc) for loc in finders.searched_locations])
|
||||
"\nLooking in the following locations:\n %s" %
|
||||
"\n ".join([str(loc) for loc in finders.searched_locations])
|
||||
)
|
||||
else:
|
||||
searched_locations = ""
|
||||
searched_locations = ''
|
||||
if result:
|
||||
if not isinstance(result, (list, tuple)):
|
||||
result = [result]
|
||||
result = (os.path.realpath(path) for path in result)
|
||||
if verbosity >= 1:
|
||||
file_list = "\n ".join(result)
|
||||
return "Found '%s' here:\n %s%s" % (
|
||||
path,
|
||||
file_list,
|
||||
searched_locations,
|
||||
)
|
||||
file_list = '\n '.join(result)
|
||||
return ("Found '%s' here:\n %s%s" %
|
||||
(path, file_list, searched_locations))
|
||||
else:
|
||||
return "\n".join(result)
|
||||
return '\n'.join(result)
|
||||
else:
|
||||
message = ["No matching file found for '%s'." % path]
|
||||
if verbosity >= 2:
|
||||
message.append(searched_locations)
|
||||
if verbosity >= 1:
|
||||
self.stderr.write("\n".join(message))
|
||||
self.stderr.write('\n'.join(message))
|
||||
|
||||
+10
-14
@@ -1,26 +1,22 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.handlers import StaticFilesHandler
|
||||
from django.core.management.commands.runserver import Command as RunserverCommand
|
||||
from django.core.management.commands.runserver import (
|
||||
Command as RunserverCommand,
|
||||
)
|
||||
|
||||
|
||||
class Command(RunserverCommand):
|
||||
help = (
|
||||
"Starts a lightweight web server for development and also serves static files."
|
||||
)
|
||||
help = "Starts a lightweight Web server for development and also serves static files."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--nostatic",
|
||||
action="store_false",
|
||||
dest="use_static_handler",
|
||||
help="Tells Django to NOT automatically serve static files at STATIC_URL.",
|
||||
'--nostatic', action="store_false", dest='use_static_handler',
|
||||
help='Tells Django to NOT automatically serve static files at STATIC_URL.',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--insecure",
|
||||
action="store_true",
|
||||
dest="insecure_serving",
|
||||
help="Allows serving static files even if DEBUG is False.",
|
||||
'--insecure', action="store_true", dest='insecure_serving',
|
||||
help='Allows serving static files even if DEBUG is False.',
|
||||
)
|
||||
|
||||
def get_handler(self, *args, **options):
|
||||
@@ -29,8 +25,8 @@ class Command(RunserverCommand):
|
||||
if static files should be served. Otherwise return the default handler.
|
||||
"""
|
||||
handler = super().get_handler(*args, **options)
|
||||
use_static_handler = options["use_static_handler"]
|
||||
insecure_serving = options["insecure_serving"]
|
||||
use_static_handler = options['use_static_handler']
|
||||
insecure_serving = options['insecure_serving']
|
||||
if use_static_handler and (settings.DEBUG or insecure_serving):
|
||||
return StaticFilesHandler(handler)
|
||||
return handler
|
||||
|
||||
@@ -20,7 +20,6 @@ class StaticFilesStorage(FileSystemStorage):
|
||||
The defaults for ``location`` and ``base_url`` are
|
||||
``STATIC_ROOT`` and ``STATIC_URL``.
|
||||
"""
|
||||
|
||||
def __init__(self, location=None, base_url=None, *args, **kwargs):
|
||||
if location is None:
|
||||
location = settings.STATIC_ROOT
|
||||
@@ -36,37 +35,20 @@ class StaticFilesStorage(FileSystemStorage):
|
||||
|
||||
def path(self, name):
|
||||
if not self.location:
|
||||
raise ImproperlyConfigured(
|
||||
"You're using the staticfiles app "
|
||||
"without having set the STATIC_ROOT "
|
||||
"setting to a filesystem path."
|
||||
)
|
||||
raise ImproperlyConfigured("You're using the staticfiles app "
|
||||
"without having set the STATIC_ROOT "
|
||||
"setting to a filesystem path.")
|
||||
return super().path(name)
|
||||
|
||||
|
||||
class HashedFilesMixin:
|
||||
default_template = """url("%(url)s")"""
|
||||
default_template = """url("%s")"""
|
||||
max_post_process_passes = 5
|
||||
patterns = (
|
||||
(
|
||||
"*.css",
|
||||
(
|
||||
r"""(?P<matched>url\(['"]{0,1}\s*(?P<url>.*?)["']{0,1}\))""",
|
||||
(
|
||||
r"""(?P<matched>@import\s*["']\s*(?P<url>.*?)["'])""",
|
||||
"""@import url("%(url)s")""",
|
||||
),
|
||||
),
|
||||
),
|
||||
(
|
||||
"*.js",
|
||||
(
|
||||
(
|
||||
r"(?m)(?P<matched>)^(//# (?-i:sourceMappingURL)=(?P<url>.*))$",
|
||||
"//# sourceMappingURL=%(url)s",
|
||||
),
|
||||
),
|
||||
),
|
||||
("*.css", (
|
||||
r"""(url\(['"]{0,1}\s*(.*?)["']{0,1}\))""",
|
||||
(r"""(@import\s*["']\s*(.*?)["'])""", """@import url("%s")"""),
|
||||
)),
|
||||
)
|
||||
keep_intermediate_files = True
|
||||
|
||||
@@ -103,9 +85,7 @@ class HashedFilesMixin:
|
||||
opened = content is None
|
||||
if opened:
|
||||
if not self.exists(filename):
|
||||
raise ValueError(
|
||||
"The file '%s' could not be found with %r." % (filename, self)
|
||||
)
|
||||
raise ValueError("The file '%s' could not be found with %r." % (filename, self))
|
||||
try:
|
||||
content = self.open(filename)
|
||||
except OSError:
|
||||
@@ -118,14 +98,15 @@ class HashedFilesMixin:
|
||||
content.close()
|
||||
path, filename = os.path.split(clean_name)
|
||||
root, ext = os.path.splitext(filename)
|
||||
file_hash = (".%s" % file_hash) if file_hash else ""
|
||||
hashed_name = os.path.join(path, "%s%s%s" % (root, file_hash, ext))
|
||||
file_hash = ('.%s' % file_hash) if file_hash else ''
|
||||
hashed_name = os.path.join(path, "%s%s%s" %
|
||||
(root, file_hash, ext))
|
||||
unparsed_name = list(parsed_name)
|
||||
unparsed_name[2] = hashed_name
|
||||
# Special casing for a @font-face hack, like url(myfont.eot?#iefix")
|
||||
# http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax
|
||||
if "?#" in name and not unparsed_name[3]:
|
||||
unparsed_name[2] += "?"
|
||||
if '?#' in name and not unparsed_name[3]:
|
||||
unparsed_name[2] += '?'
|
||||
return urlunsplit(unparsed_name)
|
||||
|
||||
def _url(self, hashed_name_func, name, force=False, hashed_files=None):
|
||||
@@ -133,10 +114,10 @@ class HashedFilesMixin:
|
||||
Return the non-hashed URL in DEBUG mode.
|
||||
"""
|
||||
if settings.DEBUG and not force:
|
||||
hashed_name, fragment = name, ""
|
||||
hashed_name, fragment = name, ''
|
||||
else:
|
||||
clean_name, fragment = urldefrag(name)
|
||||
if urlsplit(clean_name).path.endswith("/"): # don't hash paths
|
||||
if urlsplit(clean_name).path.endswith('/'): # don't hash paths
|
||||
hashed_name = name
|
||||
else:
|
||||
args = (clean_name,)
|
||||
@@ -148,13 +129,13 @@ class HashedFilesMixin:
|
||||
|
||||
# Special casing for a @font-face hack, like url(myfont.eot?#iefix")
|
||||
# http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax
|
||||
query_fragment = "?#" in name # [sic!]
|
||||
query_fragment = '?#' in name # [sic!]
|
||||
if fragment or query_fragment:
|
||||
urlparts = list(urlsplit(final_url))
|
||||
if fragment and not urlparts[4]:
|
||||
urlparts[4] = fragment
|
||||
if query_fragment and not urlparts[3]:
|
||||
urlparts[2] += "?"
|
||||
urlparts[2] += '?'
|
||||
final_url = urlunsplit(urlparts)
|
||||
|
||||
return unquote(final_url)
|
||||
@@ -179,50 +160,43 @@ class HashedFilesMixin:
|
||||
This requires figuring out which files the matched URL resolves
|
||||
to and calling the url() method of the storage.
|
||||
"""
|
||||
matches = matchobj.groupdict()
|
||||
matched = matches["matched"]
|
||||
url = matches["url"]
|
||||
matched, url = matchobj.groups()
|
||||
|
||||
# Ignore absolute/protocol-relative and data-uri URLs.
|
||||
if re.match(r"^[a-z]+:", url):
|
||||
if re.match(r'^[a-z]+:', url):
|
||||
return matched
|
||||
|
||||
# Ignore absolute URLs that don't point to a static file (dynamic
|
||||
# CSS / JS?). Note that STATIC_URL cannot be empty.
|
||||
if url.startswith("/") and not url.startswith(settings.STATIC_URL):
|
||||
if url.startswith('/') and not url.startswith(settings.STATIC_URL):
|
||||
return matched
|
||||
|
||||
# Strip off the fragment so a path-like fragment won't interfere.
|
||||
url_path, fragment = urldefrag(url)
|
||||
|
||||
if url_path.startswith("/"):
|
||||
if url_path.startswith('/'):
|
||||
# Otherwise the condition above would have returned prematurely.
|
||||
assert url_path.startswith(settings.STATIC_URL)
|
||||
target_name = url_path[len(settings.STATIC_URL) :]
|
||||
target_name = url_path[len(settings.STATIC_URL):]
|
||||
else:
|
||||
# We're using the posixpath module to mix paths and URLs conveniently.
|
||||
source_name = name if os.sep == "/" else name.replace(os.sep, "/")
|
||||
source_name = name if os.sep == '/' else name.replace(os.sep, '/')
|
||||
target_name = posixpath.join(posixpath.dirname(source_name), url_path)
|
||||
|
||||
# Determine the hashed name of the target file with the storage backend.
|
||||
hashed_url = self._url(
|
||||
self._stored_name,
|
||||
unquote(target_name),
|
||||
force=True,
|
||||
hashed_files=hashed_files,
|
||||
self._stored_name, unquote(target_name),
|
||||
force=True, hashed_files=hashed_files,
|
||||
)
|
||||
|
||||
transformed_url = "/".join(
|
||||
url_path.split("/")[:-1] + hashed_url.split("/")[-1:]
|
||||
)
|
||||
transformed_url = '/'.join(url_path.split('/')[:-1] + hashed_url.split('/')[-1:])
|
||||
|
||||
# Restore the fragment that was stripped off earlier.
|
||||
if fragment:
|
||||
transformed_url += ("?#" if "?#" in url else "#") + fragment
|
||||
transformed_url += ('?#' if '?#' in url else '#') + fragment
|
||||
|
||||
# Return the hashed version to the file
|
||||
matches["url"] = unquote(transformed_url)
|
||||
return template % matches
|
||||
return template % unquote(transformed_url)
|
||||
|
||||
return converter
|
||||
|
||||
@@ -249,46 +223,31 @@ class HashedFilesMixin:
|
||||
|
||||
# build a list of adjustable files
|
||||
adjustable_paths = [
|
||||
path for path in paths if matches_patterns(path, self._patterns)
|
||||
path for path in paths
|
||||
if matches_patterns(path, self._patterns)
|
||||
]
|
||||
|
||||
# Adjustable files to yield at end, keyed by the original path.
|
||||
processed_adjustable_paths = {}
|
||||
|
||||
# Do a single pass first. Post-process all files once, yielding not
|
||||
# adjustable files and exceptions, and collecting adjustable files.
|
||||
for name, hashed_name, processed, _ in self._post_process(
|
||||
paths, adjustable_paths, hashed_files
|
||||
):
|
||||
if name not in adjustable_paths or isinstance(processed, Exception):
|
||||
yield name, hashed_name, processed
|
||||
else:
|
||||
processed_adjustable_paths[name] = (name, hashed_name, processed)
|
||||
# Do a single pass first. Post-process all files once, then repeat for
|
||||
# adjustable files.
|
||||
for name, hashed_name, processed, _ in self._post_process(paths, adjustable_paths, hashed_files):
|
||||
yield name, hashed_name, processed
|
||||
|
||||
paths = {path: paths[path] for path in adjustable_paths}
|
||||
substitutions = False
|
||||
|
||||
for i in range(self.max_post_process_passes):
|
||||
substitutions = False
|
||||
for name, hashed_name, processed, subst in self._post_process(
|
||||
paths, adjustable_paths, hashed_files
|
||||
):
|
||||
# Overwrite since hashed_name may be newer.
|
||||
processed_adjustable_paths[name] = (name, hashed_name, processed)
|
||||
for name, hashed_name, processed, subst in self._post_process(paths, adjustable_paths, hashed_files):
|
||||
yield name, hashed_name, processed
|
||||
substitutions = substitutions or subst
|
||||
|
||||
if not substitutions:
|
||||
break
|
||||
|
||||
if substitutions:
|
||||
yield "All", None, RuntimeError("Max post-process passes exceeded.")
|
||||
yield 'All', None, RuntimeError('Max post-process passes exceeded.')
|
||||
|
||||
# Store the processed paths
|
||||
self.hashed_files.update(hashed_files)
|
||||
|
||||
# Yield adjustable files with final, hashed name.
|
||||
yield from processed_adjustable_paths.values()
|
||||
|
||||
def _post_process(self, paths, adjustable_paths, hashed_files):
|
||||
# Sort the files by directory level
|
||||
def path_level(name):
|
||||
@@ -311,7 +270,7 @@ class HashedFilesMixin:
|
||||
hashed_name = hashed_files[hash_key]
|
||||
|
||||
# then get the original's file content..
|
||||
if hasattr(original_file, "seek"):
|
||||
if hasattr(original_file, 'seek'):
|
||||
original_file.seek(0)
|
||||
|
||||
hashed_file_exists = self.exists(hashed_name)
|
||||
@@ -320,13 +279,11 @@ class HashedFilesMixin:
|
||||
# ..to apply each replacement pattern to the content
|
||||
if name in adjustable_paths:
|
||||
old_hashed_name = hashed_name
|
||||
content = original_file.read().decode("utf-8")
|
||||
content = original_file.read().decode('utf-8')
|
||||
for extension, patterns in self._patterns.items():
|
||||
if matches_patterns(path, (extension,)):
|
||||
for pattern, template in patterns:
|
||||
converter = self.url_converter(
|
||||
name, hashed_files, template
|
||||
)
|
||||
converter = self.url_converter(name, hashed_files, template)
|
||||
try:
|
||||
content = pattern.sub(converter, content)
|
||||
except ValueError as exc:
|
||||
@@ -364,7 +321,7 @@ class HashedFilesMixin:
|
||||
yield name, hashed_name, processed, substitutions
|
||||
|
||||
def clean_name(self, name):
|
||||
return name.replace("\\", "/")
|
||||
return name.replace('\\', '/')
|
||||
|
||||
def hash_key(self, name):
|
||||
return name
|
||||
@@ -406,21 +363,18 @@ class HashedFilesMixin:
|
||||
|
||||
|
||||
class ManifestFilesMixin(HashedFilesMixin):
|
||||
manifest_version = "1.0" # the manifest format standard
|
||||
manifest_name = "staticfiles.json"
|
||||
manifest_version = '1.0' # the manifest format standard
|
||||
manifest_name = 'staticfiles.json'
|
||||
manifest_strict = True
|
||||
keep_intermediate_files = False
|
||||
|
||||
def __init__(self, *args, manifest_storage=None, **kwargs):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if manifest_storage is None:
|
||||
manifest_storage = self
|
||||
self.manifest_storage = manifest_storage
|
||||
self.hashed_files = self.load_manifest()
|
||||
|
||||
def read_manifest(self):
|
||||
try:
|
||||
with self.manifest_storage.open(self.manifest_name) as manifest:
|
||||
with self.open(self.manifest_name) as manifest:
|
||||
return manifest.read().decode()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
@@ -434,26 +388,24 @@ class ManifestFilesMixin(HashedFilesMixin):
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
else:
|
||||
version = stored.get("version")
|
||||
if version == "1.0":
|
||||
return stored.get("paths", {})
|
||||
raise ValueError(
|
||||
"Couldn't load manifest '%s' (version %s)"
|
||||
% (self.manifest_name, self.manifest_version)
|
||||
)
|
||||
version = stored.get('version')
|
||||
if version == '1.0':
|
||||
return stored.get('paths', {})
|
||||
raise ValueError("Couldn't load manifest '%s' (version %s)" %
|
||||
(self.manifest_name, self.manifest_version))
|
||||
|
||||
def post_process(self, *args, **kwargs):
|
||||
self.hashed_files = {}
|
||||
yield from super().post_process(*args, **kwargs)
|
||||
if not kwargs.get("dry_run"):
|
||||
if not kwargs.get('dry_run'):
|
||||
self.save_manifest()
|
||||
|
||||
def save_manifest(self):
|
||||
payload = {"paths": self.hashed_files, "version": self.manifest_version}
|
||||
if self.manifest_storage.exists(self.manifest_name):
|
||||
self.manifest_storage.delete(self.manifest_name)
|
||||
payload = {'paths': self.hashed_files, 'version': self.manifest_version}
|
||||
if self.exists(self.manifest_name):
|
||||
self.delete(self.manifest_name)
|
||||
contents = json.dumps(payload).encode()
|
||||
self.manifest_storage._save(self.manifest_name, ContentFile(contents))
|
||||
self._save(self.manifest_name, ContentFile(contents))
|
||||
|
||||
def stored_name(self, name):
|
||||
parsed_name = urlsplit(unquote(name))
|
||||
@@ -462,16 +414,14 @@ class ManifestFilesMixin(HashedFilesMixin):
|
||||
cache_name = self.hashed_files.get(hash_key)
|
||||
if cache_name is None:
|
||||
if self.manifest_strict:
|
||||
raise ValueError(
|
||||
"Missing staticfiles manifest entry for '%s'" % clean_name
|
||||
)
|
||||
raise ValueError("Missing staticfiles manifest entry for '%s'" % clean_name)
|
||||
cache_name = self.clean_name(self.hashed_name(name))
|
||||
unparsed_name = list(parsed_name)
|
||||
unparsed_name[2] = cache_name
|
||||
# Special casing for a @font-face hack, like url(myfont.eot?#iefix")
|
||||
# http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax
|
||||
if "?#" in name and not unparsed_name[3]:
|
||||
unparsed_name[2] += "?"
|
||||
if '?#' in name and not unparsed_name[3]:
|
||||
unparsed_name[2] += '?'
|
||||
return urlunsplit(unparsed_name)
|
||||
|
||||
|
||||
@@ -480,7 +430,6 @@ class ManifestStaticFilesStorage(ManifestFilesMixin, StaticFilesStorage):
|
||||
A static file system storage backend which also saves
|
||||
hashed copies of the files it saves.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ def matches_patterns(path, patterns):
|
||||
return any(fnmatch.fnmatchcase(path, pattern) for pattern in patterns)
|
||||
|
||||
|
||||
def get_files(storage, ignore_patterns=None, location=""):
|
||||
def get_files(storage, ignore_patterns=None, location=''):
|
||||
"""
|
||||
Recursively walk the storage directories yielding the paths
|
||||
of all files that should be copied.
|
||||
@@ -48,24 +48,16 @@ def check_settings(base_url=None):
|
||||
if not base_url:
|
||||
raise ImproperlyConfigured(
|
||||
"You're using the staticfiles app "
|
||||
"without having set the required STATIC_URL setting."
|
||||
)
|
||||
"without having set the required STATIC_URL setting.")
|
||||
if settings.MEDIA_URL == base_url:
|
||||
raise ImproperlyConfigured(
|
||||
"The MEDIA_URL and STATIC_URL settings must have different values"
|
||||
)
|
||||
if (
|
||||
settings.DEBUG
|
||||
and settings.MEDIA_URL
|
||||
and settings.STATIC_URL
|
||||
and settings.MEDIA_URL.startswith(settings.STATIC_URL)
|
||||
):
|
||||
raise ImproperlyConfigured("The MEDIA_URL and STATIC_URL "
|
||||
"settings must have different values")
|
||||
if (settings.DEBUG and settings.MEDIA_URL and settings.STATIC_URL and
|
||||
settings.MEDIA_URL.startswith(settings.STATIC_URL)):
|
||||
raise ImproperlyConfigured(
|
||||
"runserver can't serve media if MEDIA_URL is within STATIC_URL."
|
||||
)
|
||||
if (settings.MEDIA_ROOT and settings.STATIC_ROOT) and (
|
||||
settings.MEDIA_ROOT == settings.STATIC_ROOT
|
||||
):
|
||||
raise ImproperlyConfigured(
|
||||
"The MEDIA_ROOT and STATIC_ROOT settings must have different values"
|
||||
)
|
||||
if ((settings.MEDIA_ROOT and settings.STATIC_ROOT) and
|
||||
(settings.MEDIA_ROOT == settings.STATIC_ROOT)):
|
||||
raise ImproperlyConfigured("The MEDIA_ROOT and STATIC_ROOT "
|
||||
"settings must have different values")
|
||||
|
||||
@@ -29,10 +29,10 @@ def serve(request, path, insecure=False, **kwargs):
|
||||
"""
|
||||
if not settings.DEBUG and not insecure:
|
||||
raise Http404
|
||||
normalized_path = posixpath.normpath(path).lstrip("/")
|
||||
normalized_path = posixpath.normpath(path).lstrip('/')
|
||||
absolute_path = finders.find(normalized_path)
|
||||
if not absolute_path:
|
||||
if path.endswith("/") or path == "":
|
||||
if path.endswith('/') or path == '':
|
||||
raise Http404("Directory indexes are not allowed here.")
|
||||
raise Http404("'%s' could not be found" % path)
|
||||
document_root, path = os.path.split(absolute_path)
|
||||
|
||||
Reference in New Issue
Block a user