Fixed the staticfiles management commands collectstatic and findstatic to not raise encoding related exceptions when handlings filenames with non-ASCII characters.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@15538 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2011-02-14 23:45:32 +00:00
parent 6eacbfd411
commit 64a0a33c33
4 changed files with 37 additions and 20 deletions

View File

@ -6,6 +6,7 @@ from optparse import make_option
from django.conf import settings from django.conf import settings
from django.core.files.storage import get_storage_class from django.core.files.storage import get_storage_class
from django.core.management.base import CommandError, NoArgsCommand from django.core.management.base import CommandError, NoArgsCommand
from django.utils.encoding import smart_str
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
@ -64,7 +65,7 @@ class Command(NoArgsCommand):
# Warn before doing anything more. # Warn before doing anything more.
if options.get('interactive'): if options.get('interactive'):
confirm = raw_input(""" confirm = raw_input(u"""
You have requested to collect static files at the destination You have requested to collect static files at the destination
location as specified in your settings file ('%s'). location as specified in your settings file ('%s').
@ -90,17 +91,18 @@ Type 'yes' to continue, or 'no' to cancel: """ % settings.STATIC_ROOT)
actual_count = len(self.copied_files) + len(self.symlinked_files) actual_count = len(self.copied_files) + len(self.symlinked_files)
unmodified_count = len(self.unmodified_files) unmodified_count = len(self.unmodified_files)
if self.verbosity >= 1: if self.verbosity >= 1:
self.stdout.write("\n%s static file%s %s to '%s'%s.\n" self.stdout.write(smart_str(u"\n%s static file%s %s to '%s'%s.\n"
% (actual_count, actual_count != 1 and 's' or '', % (actual_count, actual_count != 1 and 's' or '',
symlink and 'symlinked' or 'copied', symlink and 'symlinked' or 'copied',
settings.STATIC_ROOT, settings.STATIC_ROOT,
unmodified_count and ' (%s unmodified)' unmodified_count and ' (%s unmodified)'
% unmodified_count or '')) % unmodified_count or '')))
def log(self, msg, level=2): def log(self, msg, level=2):
""" """
Small log helper Small log helper
""" """
msg = smart_str(msg)
if not msg.endswith("\n"): if not msg.endswith("\n"):
msg += "\n" msg += "\n"
if self.verbosity >= level: if self.verbosity >= level:
@ -135,13 +137,13 @@ Type 'yes' to continue, or 'no' to cancel: """ % settings.STATIC_ROOT)
(not symlink and full_path and os.path.islink(full_path))): (not symlink and full_path and os.path.islink(full_path))):
if prefixed_path not in self.unmodified_files: if prefixed_path not in self.unmodified_files:
self.unmodified_files.append(prefixed_path) self.unmodified_files.append(prefixed_path)
self.log("Skipping '%s' (not modified)" % path) self.log(u"Skipping '%s' (not modified)" % path)
return False return False
# Then delete the existing file if really needed # Then delete the existing file if really needed
if options['dry_run']: if options['dry_run']:
self.log("Pretending to delete '%s'" % path) self.log(u"Pretending to delete '%s'" % path)
else: else:
self.log("Deleting '%s'" % path) self.log(u"Deleting '%s'" % path)
self.storage.delete(prefixed_path) self.storage.delete(prefixed_path)
return True return True
@ -151,7 +153,7 @@ Type 'yes' to continue, or 'no' to cancel: """ % settings.STATIC_ROOT)
""" """
# Skip this file if it was already copied earlier # Skip this file if it was already copied earlier
if prefixed_path in self.symlinked_files: if prefixed_path in self.symlinked_files:
return self.log("Skipping '%s' (already linked earlier)" % path) return self.log(u"Skipping '%s' (already linked earlier)" % path)
# Delete the target file if needed or break # Delete the target file if needed or break
if not self.delete_file(path, prefixed_path, source_storage, **options): if not self.delete_file(path, prefixed_path, source_storage, **options):
return return
@ -159,9 +161,9 @@ Type 'yes' to continue, or 'no' to cancel: """ % settings.STATIC_ROOT)
source_path = source_storage.path(path) source_path = source_storage.path(path)
# Finally link the file # Finally link the file
if options['dry_run']: if options['dry_run']:
self.log("Pretending to link '%s'" % source_path, level=1) self.log(u"Pretending to link '%s'" % source_path, level=1)
else: else:
self.log("Linking '%s'" % source_path, level=1) self.log(u"Linking '%s'" % source_path, level=1)
full_path = self.storage.path(prefixed_path) full_path = self.storage.path(prefixed_path)
try: try:
os.makedirs(os.path.dirname(full_path)) os.makedirs(os.path.dirname(full_path))
@ -177,7 +179,7 @@ Type 'yes' to continue, or 'no' to cancel: """ % settings.STATIC_ROOT)
""" """
# Skip this file if it was already copied earlier # Skip this file if it was already copied earlier
if prefixed_path in self.copied_files: if prefixed_path in self.copied_files:
return self.log("Skipping '%s' (already copied earlier)" % path) return self.log(u"Skipping '%s' (already copied earlier)" % path)
# Delete the target file if needed or break # Delete the target file if needed or break
if not self.delete_file(path, prefixed_path, source_storage, **options): if not self.delete_file(path, prefixed_path, source_storage, **options):
return return
@ -185,9 +187,9 @@ Type 'yes' to continue, or 'no' to cancel: """ % settings.STATIC_ROOT)
source_path = source_storage.path(path) source_path = source_storage.path(path)
# Finally start copying # Finally start copying
if options['dry_run']: if options['dry_run']:
self.log("Pretending to copy '%s'" % source_path, level=1) self.log(u"Pretending to copy '%s'" % source_path, level=1)
else: else:
self.log("Copying '%s'" % source_path, level=1) self.log(u"Copying '%s'" % source_path, level=1)
if self.local: if self.local:
full_path = self.storage.path(prefixed_path) full_path = self.storage.path(prefixed_path)
try: try:

View File

@ -1,6 +1,7 @@
import os import os
from optparse import make_option from optparse import make_option
from django.core.management.base import LabelCommand from django.core.management.base import LabelCommand
from django.utils.encoding import smart_str, smart_unicode
from django.contrib.staticfiles import finders from django.contrib.staticfiles import finders
@ -16,11 +17,15 @@ class Command(LabelCommand):
def handle_label(self, path, **options): def handle_label(self, path, **options):
verbosity = int(options.get('verbosity', 1)) verbosity = int(options.get('verbosity', 1))
result = finders.find(path, all=options['all']) result = finders.find(path, all=options['all'])
path = smart_unicode(path)
if result: if result:
if not isinstance(result, (list, tuple)): if not isinstance(result, (list, tuple)):
result = [result] result = [result]
output = '\n '.join((os.path.realpath(path) for path in result)) output = u'\n '.join(
self.stdout.write("Found %r here:\n %s\n" % (path, output)) (smart_unicode(os.path.realpath(path)) for path in result))
self.stdout.write(
smart_str(u"Found '%s' here:\n %s\n" % (path, output)))
else: else:
if verbosity >= 1: if verbosity >= 1:
self.stdout.write("No matching file found for %r.\n" % path) self.stderr.write(
smart_str("No matching file found for '%s'.\n" % path))

View File

@ -0,0 +1 @@
speçial in the app dir

View File

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*-
import codecs
import os import os
import posixpath import posixpath
import shutil import shutil
@ -11,6 +13,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.management import call_command from django.core.management import call_command
from django.test import TestCase from django.test import TestCase
from django.utils.encoding import smart_unicode
from django.utils._os import rmtree_errorhandler from django.utils._os import rmtree_errorhandler
@ -72,8 +75,8 @@ class StaticFilesTestCase(TestCase):
settings.INSTALLED_APPS = self.old_installed_apps settings.INSTALLED_APPS = self.old_installed_apps
def assertFileContains(self, filepath, text): def assertFileContains(self, filepath, text):
self.assertTrue(text in self._get_file(filepath), self.assertTrue(text in self._get_file(smart_unicode(filepath)),
"'%s' not in '%s'" % (text, filepath)) u"'%s' not in '%s'" % (text, filepath))
def assertFileNotFound(self, filepath): def assertFileNotFound(self, filepath):
self.assertRaises(IOError, self._get_file, filepath) self.assertRaises(IOError, self._get_file, filepath)
@ -108,7 +111,7 @@ class BuildStaticTestCase(StaticFilesTestCase):
def _get_file(self, filepath): def _get_file(self, filepath):
assert filepath, 'filepath is empty.' assert filepath, 'filepath is empty.'
filepath = os.path.join(settings.STATIC_ROOT, filepath) filepath = os.path.join(settings.STATIC_ROOT, filepath)
f = open(filepath) f = codecs.open(filepath, "r", "utf-8")
try: try:
return f.read() return f.read()
finally: finally:
@ -140,10 +143,16 @@ class TestDefaults(object):
def test_app_files(self): def test_app_files(self):
""" """
Can find a file in an app media/ directory. Can find a file in an app static/ directory.
""" """
self.assertFileContains('test/file1.txt', 'file1 in the app dir') self.assertFileContains('test/file1.txt', 'file1 in the app dir')
def test_nonascii_filenames(self):
"""
Can find a file with non-ASCII character in an app static/ directory.
"""
self.assertFileContains(u'test/speçial.txt', u'speçial in the app dir')
class TestFindStatic(BuildStaticTestCase, TestDefaults): class TestFindStatic(BuildStaticTestCase, TestDefaults):
""" """
@ -156,7 +165,7 @@ class TestFindStatic(BuildStaticTestCase, TestDefaults):
call_command('findstatic', filepath, all=False, verbosity='0') call_command('findstatic', filepath, all=False, verbosity='0')
sys.stdout.seek(0) sys.stdout.seek(0)
lines = [l.strip() for l in sys.stdout.readlines()] lines = [l.strip() for l in sys.stdout.readlines()]
contents = open(lines[1].strip()).read() contents = codecs.open(lines[1].strip(), "r", "utf-8").read()
finally: finally:
sys.stdout = _stdout sys.stdout = _stdout
return contents return contents