File storage refactoring, adding far more flexibility to Django's file handling. The new files.txt document has details of the new features.
This is a backwards-incompatible change; consult BackwardsIncompatibleChanges for details. Fixes #3567, #3621, #4345, #5361, #5655, #7415. Many thanks to Marty Alchin who did the vast majority of this work. git-svn-id: http://code.djangoproject.com/svn/django/trunk@8244 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
c49eac7d4f
commit
7899568e01
|
@ -226,6 +226,9 @@ SECRET_KEY = ''
|
||||||
# Path to the "jing" executable -- needed to validate XMLFields
|
# Path to the "jing" executable -- needed to validate XMLFields
|
||||||
JING_PATH = "/usr/bin/jing"
|
JING_PATH = "/usr/bin/jing"
|
||||||
|
|
||||||
|
# Default file storage mechanism that holds media.
|
||||||
|
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||||
|
|
||||||
# Absolute path to the directory that holds media.
|
# Absolute path to the directory that holds media.
|
||||||
# Example: "/home/media/media.lawrence.com/"
|
# Example: "/home/media/media.lawrence.com/"
|
||||||
MEDIA_ROOT = ''
|
MEDIA_ROOT = ''
|
||||||
|
|
|
@ -85,8 +85,8 @@ class AdminFileWidget(forms.FileInput):
|
||||||
def render(self, name, value, attrs=None):
|
def render(self, name, value, attrs=None):
|
||||||
output = []
|
output = []
|
||||||
if value:
|
if value:
|
||||||
output.append('%s <a target="_blank" href="%s%s">%s</a> <br />%s ' % \
|
output.append('%s <a target="_blank" href="%s">%s</a> <br />%s ' % \
|
||||||
(_('Currently:'), settings.MEDIA_URL, value, value, _('Change:')))
|
(_('Currently:'), value.url, value, _('Change:')))
|
||||||
output.append(super(AdminFileWidget, self).render(name, value, attrs))
|
output.append(super(AdminFileWidget, self).render(name, value, attrs))
|
||||||
return mark_safe(u''.join(output))
|
return mark_safe(u''.join(output))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from django.core.files.base import File
|
|
@ -0,0 +1,169 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.utils.encoding import smart_str, smart_unicode
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cStringIO import StringIO
|
||||||
|
except ImportError:
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
class File(object):
|
||||||
|
DEFAULT_CHUNK_SIZE = 64 * 2**10
|
||||||
|
|
||||||
|
def __init__(self, file):
|
||||||
|
self.file = file
|
||||||
|
self._name = file.name
|
||||||
|
self._mode = file.mode
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return smart_str(self.name or '')
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return smart_unicode(self.name or u'')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<%s: %s>" % (self.__class__.__name__, self or "None")
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
return not not self.name
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.size
|
||||||
|
|
||||||
|
def _get_name(self):
|
||||||
|
return self._name
|
||||||
|
name = property(_get_name)
|
||||||
|
|
||||||
|
def _get_mode(self):
|
||||||
|
return self._mode
|
||||||
|
mode = property(_get_mode)
|
||||||
|
|
||||||
|
def _get_closed(self):
|
||||||
|
return self._closed
|
||||||
|
closed = property(_get_closed)
|
||||||
|
|
||||||
|
def _get_size(self):
|
||||||
|
if not hasattr(self, '_size'):
|
||||||
|
if hasattr(self.file, 'size'):
|
||||||
|
self._size = self.file.size
|
||||||
|
elif os.path.exists(self.file.name):
|
||||||
|
self._size = os.path.getsize(self.file.name)
|
||||||
|
else:
|
||||||
|
raise AttributeError("Unable to determine the file's size.")
|
||||||
|
return self._size
|
||||||
|
|
||||||
|
def _set_size(self, size):
|
||||||
|
self._size = size
|
||||||
|
|
||||||
|
size = property(_get_size, _set_size)
|
||||||
|
|
||||||
|
def chunks(self, chunk_size=None):
|
||||||
|
"""
|
||||||
|
Read the file and yield chucks of ``chunk_size`` bytes (defaults to
|
||||||
|
``UploadedFile.DEFAULT_CHUNK_SIZE``).
|
||||||
|
"""
|
||||||
|
if not chunk_size:
|
||||||
|
chunk_size = self.__class__.DEFAULT_CHUNK_SIZE
|
||||||
|
|
||||||
|
if hasattr(self, 'seek'):
|
||||||
|
self.seek(0)
|
||||||
|
# Assume the pointer is at zero...
|
||||||
|
counter = self.size
|
||||||
|
|
||||||
|
while counter > 0:
|
||||||
|
yield self.read(chunk_size)
|
||||||
|
counter -= chunk_size
|
||||||
|
|
||||||
|
def multiple_chunks(self, chunk_size=None):
|
||||||
|
"""
|
||||||
|
Returns ``True`` if you can expect multiple chunks.
|
||||||
|
|
||||||
|
NB: If a particular file representation is in memory, subclasses should
|
||||||
|
always return ``False`` -- there's no good reason to read from memory in
|
||||||
|
chunks.
|
||||||
|
"""
|
||||||
|
if not chunk_size:
|
||||||
|
chunk_size = self.DEFAULT_CHUNK_SIZE
|
||||||
|
return self.size > chunk_size
|
||||||
|
|
||||||
|
def xreadlines(self):
|
||||||
|
return iter(self)
|
||||||
|
|
||||||
|
def readlines(self):
|
||||||
|
return list(self.xreadlines())
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
# Iterate over this file-like object by newlines
|
||||||
|
buffer_ = None
|
||||||
|
for chunk in self.chunks():
|
||||||
|
chunk_buffer = StringIO(chunk)
|
||||||
|
|
||||||
|
for line in chunk_buffer:
|
||||||
|
if buffer_:
|
||||||
|
line = buffer_ + line
|
||||||
|
buffer_ = None
|
||||||
|
|
||||||
|
# If this is the end of a line, yield
|
||||||
|
# otherwise, wait for the next round
|
||||||
|
if line[-1] in ('\n', '\r'):
|
||||||
|
yield line
|
||||||
|
else:
|
||||||
|
buffer_ = line
|
||||||
|
|
||||||
|
if buffer_ is not None:
|
||||||
|
yield buffer_
|
||||||
|
|
||||||
|
def open(self, mode=None):
|
||||||
|
if not self.closed:
|
||||||
|
self.seek(0)
|
||||||
|
elif os.path.exists(self.file.name):
|
||||||
|
self.file = open(self.file.name, mode or self.file.mode)
|
||||||
|
else:
|
||||||
|
raise ValueError("The file cannot be reopened.")
|
||||||
|
|
||||||
|
def seek(self, position):
|
||||||
|
self.file.seek(position)
|
||||||
|
|
||||||
|
def tell(self):
|
||||||
|
return self.file.tell()
|
||||||
|
|
||||||
|
def read(self, num_bytes=None):
|
||||||
|
if num_bytes is None:
|
||||||
|
return self.file.read()
|
||||||
|
return self.file.read(num_bytes)
|
||||||
|
|
||||||
|
def write(self, content):
|
||||||
|
if not self.mode.startswith('w'):
|
||||||
|
raise IOError("File was not opened with write access.")
|
||||||
|
self.file.write(content)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
if not self.mode.startswith('w'):
|
||||||
|
raise IOError("File was not opened with write access.")
|
||||||
|
self.file.flush()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.file.close()
|
||||||
|
self._closed = True
|
||||||
|
|
||||||
|
class ContentFile(File):
|
||||||
|
"""
|
||||||
|
A File-like object that takes just raw content, rather than an actual file.
|
||||||
|
"""
|
||||||
|
def __init__(self, content):
|
||||||
|
self.file = StringIO(content or '')
|
||||||
|
self.size = len(content or '')
|
||||||
|
self.file.seek(0)
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'Raw content'
|
||||||
|
|
||||||
|
def __nonzero__(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def open(self, mode=None):
|
||||||
|
if self._closed:
|
||||||
|
self._closed = False
|
||||||
|
self.seek(0)
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""
|
||||||
|
Utility functions for handling images.
|
||||||
|
|
||||||
|
Requires PIL, as you might imagine.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PIL import ImageFile as PIL
|
||||||
|
from django.core.files import File
|
||||||
|
|
||||||
|
class ImageFile(File):
|
||||||
|
"""
|
||||||
|
A mixin for use alongside django.core.files.base.File, which provides
|
||||||
|
additional features for dealing with images.
|
||||||
|
"""
|
||||||
|
def _get_width(self):
|
||||||
|
return self._get_image_dimensions()[0]
|
||||||
|
width = property(_get_width)
|
||||||
|
|
||||||
|
def _get_height(self):
|
||||||
|
return self._get_image_dimensions()[1]
|
||||||
|
height = property(_get_height)
|
||||||
|
|
||||||
|
def _get_image_dimensions(self):
|
||||||
|
if not hasattr(self, '_dimensions_cache'):
|
||||||
|
self._dimensions_cache = get_image_dimensions(self)
|
||||||
|
return self._dimensions_cache
|
||||||
|
|
||||||
|
def get_image_dimensions(file_or_path):
|
||||||
|
"""Returns the (width, height) of an image, given an open file or a path."""
|
||||||
|
p = PIL.Parser()
|
||||||
|
if hasattr(file_or_path, 'read'):
|
||||||
|
file = file_or_path
|
||||||
|
else:
|
||||||
|
file = open(file_or_path, 'rb')
|
||||||
|
while 1:
|
||||||
|
data = file.read(1024)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
p.feed(data)
|
||||||
|
if p.image:
|
||||||
|
return p.image.size
|
||||||
|
return None
|
|
@ -0,0 +1,214 @@
|
||||||
|
import os
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
|
||||||
|
from django.utils.encoding import force_unicode, smart_str
|
||||||
|
from django.utils.text import force_unicode, get_valid_filename
|
||||||
|
from django.utils._os import safe_join
|
||||||
|
from django.core.files import locks, File
|
||||||
|
|
||||||
|
__all__ = ('Storage', 'FileSystemStorage', 'DefaultStorage', 'default_storage')
|
||||||
|
|
||||||
|
class Storage(object):
|
||||||
|
"""
|
||||||
|
A base storage class, providing some default behaviors that all other
|
||||||
|
storage systems can inherit or override, as necessary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The following methods represent a public interface to private methods.
|
||||||
|
# These shouldn't be overridden by subclasses unless absolutely necessary.
|
||||||
|
|
||||||
|
def open(self, name, mode='rb', mixin=None):
|
||||||
|
"""
|
||||||
|
Retrieves the specified file from storage, using the optional mixin
|
||||||
|
class to customize what features are available on the File returned.
|
||||||
|
"""
|
||||||
|
file = self._open(name, mode)
|
||||||
|
if mixin:
|
||||||
|
# Add the mixin as a parent class of the File returned from storage.
|
||||||
|
file.__class__ = type(mixin.__name__, (mixin, file.__class__), {})
|
||||||
|
return file
|
||||||
|
|
||||||
|
def save(self, name, content):
|
||||||
|
"""
|
||||||
|
Saves new content to the file specified by name. The content should be a
|
||||||
|
proper File object, ready to be read from the beginning.
|
||||||
|
"""
|
||||||
|
# Check for old-style usage. Warn here first since there are multiple
|
||||||
|
# locations where we need to support both new and old usage.
|
||||||
|
if isinstance(content, basestring):
|
||||||
|
import warnings
|
||||||
|
warnings.warn(
|
||||||
|
message = "Representing files as strings is deprecated." \
|
||||||
|
"Use django.core.files.base.ContentFile instead.",
|
||||||
|
category = DeprecationWarning,
|
||||||
|
stacklevel = 2
|
||||||
|
)
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
content = ContentFile(content)
|
||||||
|
|
||||||
|
# Get the proper name for the file, as it will actually be saved.
|
||||||
|
if name is None:
|
||||||
|
name = content.name
|
||||||
|
name = self.get_available_name(name)
|
||||||
|
|
||||||
|
self._save(name, content)
|
||||||
|
|
||||||
|
# Store filenames with forward slashes, even on Windows
|
||||||
|
return force_unicode(name.replace('\\', '/'))
|
||||||
|
|
||||||
|
# These methods are part of the public API, with default implementations.
|
||||||
|
|
||||||
|
def get_valid_name(self, name):
|
||||||
|
"""
|
||||||
|
Returns a filename, based on the provided filename, that's suitable for
|
||||||
|
use in the target storage system.
|
||||||
|
"""
|
||||||
|
return get_valid_filename(name)
|
||||||
|
|
||||||
|
def get_available_name(self, name):
|
||||||
|
"""
|
||||||
|
Returns a filename that's free on the target storage system, and
|
||||||
|
available for new content to be written to.
|
||||||
|
"""
|
||||||
|
# If the filename already exists, keep adding an underscore to the name
|
||||||
|
# of the file until the filename doesn't exist.
|
||||||
|
while self.exists(name):
|
||||||
|
try:
|
||||||
|
dot_index = name.rindex('.')
|
||||||
|
except ValueError: # filename has no dot
|
||||||
|
name += '_'
|
||||||
|
else:
|
||||||
|
name = name[:dot_index] + '_' + name[dot_index:]
|
||||||
|
return name
|
||||||
|
|
||||||
|
def path(self, name):
|
||||||
|
"""
|
||||||
|
Returns a local filesystem path where the file can be retrieved using
|
||||||
|
Python's built-in open() function. Storage systems that can't be
|
||||||
|
accessed using open() should *not* implement this method.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("This backend doesn't support absolute paths.")
|
||||||
|
|
||||||
|
# The following methods form the public API for storage systems, but with
|
||||||
|
# no default implementations. Subclasses must implement *all* of these.
|
||||||
|
|
||||||
|
def delete(self, name):
|
||||||
|
"""
|
||||||
|
Deletes the specified file from the storage system.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def exists(self, name):
|
||||||
|
"""
|
||||||
|
Returns True if a file referened by the given name already exists in the
|
||||||
|
storage system, or False if the name is available for a new file.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def listdir(self, path):
|
||||||
|
"""
|
||||||
|
Lists the contents of the specified path, returning a 2-tuple of lists;
|
||||||
|
the first item being directories, the second item being files.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def size(self, name):
|
||||||
|
"""
|
||||||
|
Returns the total size, in bytes, of the file specified by name.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def url(self, name):
|
||||||
|
"""
|
||||||
|
Returns an absolute URL where the file's contents can be accessed
|
||||||
|
directly by a web browser.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
class FileSystemStorage(Storage):
|
||||||
|
"""
|
||||||
|
Standard filesystem storage
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, location=settings.MEDIA_ROOT, base_url=settings.MEDIA_URL):
|
||||||
|
self.location = os.path.abspath(location)
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
def _open(self, name, mode='rb'):
|
||||||
|
return File(open(self.path(name), mode))
|
||||||
|
|
||||||
|
def _save(self, name, content):
|
||||||
|
full_path = self.path(name)
|
||||||
|
|
||||||
|
directory = os.path.dirname(full_path)
|
||||||
|
if not os.path.exists(directory):
|
||||||
|
os.makedirs(directory)
|
||||||
|
elif not os.path.isdir(directory):
|
||||||
|
raise IOError("%s exists and is not a directory." % directory)
|
||||||
|
|
||||||
|
if hasattr(content, 'temporary_file_path'):
|
||||||
|
# This file has a file path that we can move.
|
||||||
|
file_move_safe(content.temporary_file_path(), full_path)
|
||||||
|
content.close()
|
||||||
|
else:
|
||||||
|
# This is a normal uploadedfile that we can stream.
|
||||||
|
fp = open(full_path, 'wb')
|
||||||
|
locks.lock(fp, locks.LOCK_EX)
|
||||||
|
for chunk in content.chunks():
|
||||||
|
fp.write(chunk)
|
||||||
|
locks.unlock(fp)
|
||||||
|
fp.close()
|
||||||
|
|
||||||
|
def delete(self, name):
|
||||||
|
name = self.path(name)
|
||||||
|
# If the file exists, delete it from the filesystem.
|
||||||
|
if os.path.exists(name):
|
||||||
|
os.remove(name)
|
||||||
|
|
||||||
|
def exists(self, name):
|
||||||
|
return os.path.exists(self.path(name))
|
||||||
|
|
||||||
|
def listdir(self, path):
|
||||||
|
path = self.path(path)
|
||||||
|
directories, files = [], []
|
||||||
|
for entry in os.listdir(path):
|
||||||
|
if os.path.isdir(os.path.join(path, entry)):
|
||||||
|
directories.append(entry)
|
||||||
|
else:
|
||||||
|
files.append(entry)
|
||||||
|
return directories, files
|
||||||
|
|
||||||
|
def path(self, name):
|
||||||
|
try:
|
||||||
|
path = safe_join(self.location, name)
|
||||||
|
except ValueError:
|
||||||
|
raise SuspiciousOperation("Attempted access to '%s' denied." % name)
|
||||||
|
return os.path.normpath(path)
|
||||||
|
|
||||||
|
def size(self, name):
|
||||||
|
return os.path.getsize(self.path(name))
|
||||||
|
|
||||||
|
def url(self, name):
|
||||||
|
if self.base_url is None:
|
||||||
|
raise ValueError("This file is not accessible via a URL.")
|
||||||
|
return urlparse.urljoin(self.base_url, name).replace('\\', '/')
|
||||||
|
|
||||||
|
def get_storage_class(import_path):
|
||||||
|
try:
|
||||||
|
dot = import_path.rindex('.')
|
||||||
|
except ValueError:
|
||||||
|
raise ImproperlyConfigured("%s isn't a storage module." % import_path)
|
||||||
|
module, classname = import_path[:dot], import_path[dot+1:]
|
||||||
|
try:
|
||||||
|
mod = __import__(module, {}, {}, [''])
|
||||||
|
except ImportError, e:
|
||||||
|
raise ImproperlyConfigured('Error importing storage module %s: "%s"' % (module, e))
|
||||||
|
try:
|
||||||
|
return getattr(mod, classname)
|
||||||
|
except AttributeError:
|
||||||
|
raise ImproperlyConfigured('Storage module "%s" does not define a "%s" class.' % (module, classname))
|
||||||
|
|
||||||
|
DefaultStorage = get_storage_class(settings.DEFAULT_FILE_STORAGE)
|
||||||
|
default_storage = DefaultStorage()
|
|
@ -10,6 +10,7 @@ except ImportError:
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.files.base import File
|
||||||
|
|
||||||
from django.core.files import temp as tempfile
|
from django.core.files import temp as tempfile
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ def deprecated_property(old, new, readonly=False):
|
||||||
else:
|
else:
|
||||||
return property(getter, setter)
|
return property(getter, setter)
|
||||||
|
|
||||||
class UploadedFile(object):
|
class UploadedFile(File):
|
||||||
"""
|
"""
|
||||||
A abstract uploaded file (``TemporaryUploadedFile`` and
|
A abstract uploaded file (``TemporaryUploadedFile`` and
|
||||||
``InMemoryUploadedFile`` are the built-in concrete subclasses).
|
``InMemoryUploadedFile`` are the built-in concrete subclasses).
|
||||||
|
@ -76,23 +77,6 @@ class UploadedFile(object):
|
||||||
|
|
||||||
name = property(_get_name, _set_name)
|
name = property(_get_name, _set_name)
|
||||||
|
|
||||||
def chunks(self, chunk_size=None):
|
|
||||||
"""
|
|
||||||
Read the file and yield chucks of ``chunk_size`` bytes (defaults to
|
|
||||||
``UploadedFile.DEFAULT_CHUNK_SIZE``).
|
|
||||||
"""
|
|
||||||
if not chunk_size:
|
|
||||||
chunk_size = UploadedFile.DEFAULT_CHUNK_SIZE
|
|
||||||
|
|
||||||
if hasattr(self, 'seek'):
|
|
||||||
self.seek(0)
|
|
||||||
# Assume the pointer is at zero...
|
|
||||||
counter = self.size
|
|
||||||
|
|
||||||
while counter > 0:
|
|
||||||
yield self.read(chunk_size)
|
|
||||||
counter -= chunk_size
|
|
||||||
|
|
||||||
# Deprecated properties
|
# Deprecated properties
|
||||||
filename = deprecated_property(old="filename", new="name")
|
filename = deprecated_property(old="filename", new="name")
|
||||||
file_name = deprecated_property(old="file_name", new="name")
|
file_name = deprecated_property(old="file_name", new="name")
|
||||||
|
@ -108,18 +92,6 @@ class UploadedFile(object):
|
||||||
return self.read()
|
return self.read()
|
||||||
data = property(_get_data)
|
data = property(_get_data)
|
||||||
|
|
||||||
def multiple_chunks(self, chunk_size=None):
|
|
||||||
"""
|
|
||||||
Returns ``True`` if you can expect multiple chunks.
|
|
||||||
|
|
||||||
NB: If a particular file representation is in memory, subclasses should
|
|
||||||
always return ``False`` -- there's no good reason to read from memory in
|
|
||||||
chunks.
|
|
||||||
"""
|
|
||||||
if not chunk_size:
|
|
||||||
chunk_size = UploadedFile.DEFAULT_CHUNK_SIZE
|
|
||||||
return self.size > chunk_size
|
|
||||||
|
|
||||||
# Abstract methods; subclasses *must* define read() and probably should
|
# Abstract methods; subclasses *must* define read() and probably should
|
||||||
# define open/close.
|
# define open/close.
|
||||||
def read(self, num_bytes=None):
|
def read(self, num_bytes=None):
|
||||||
|
@ -131,33 +103,6 @@ class UploadedFile(object):
|
||||||
def close(self):
|
def close(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def xreadlines(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def readlines(self):
|
|
||||||
return list(self.xreadlines())
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
# Iterate over this file-like object by newlines
|
|
||||||
buffer_ = None
|
|
||||||
for chunk in self.chunks():
|
|
||||||
chunk_buffer = StringIO(chunk)
|
|
||||||
|
|
||||||
for line in chunk_buffer:
|
|
||||||
if buffer_:
|
|
||||||
line = buffer_ + line
|
|
||||||
buffer_ = None
|
|
||||||
|
|
||||||
# If this is the end of a line, yield
|
|
||||||
# otherwise, wait for the next round
|
|
||||||
if line[-1] in ('\n', '\r'):
|
|
||||||
yield line
|
|
||||||
else:
|
|
||||||
buffer_ = line
|
|
||||||
|
|
||||||
if buffer_ is not None:
|
|
||||||
yield buffer_
|
|
||||||
|
|
||||||
# Backwards-compatible support for uploaded-files-as-dictionaries.
|
# Backwards-compatible support for uploaded-files-as-dictionaries.
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.db.models.manager import Manager
|
||||||
from django.db.models.base import Model
|
from django.db.models.base import Model
|
||||||
from django.db.models.fields import *
|
from django.db.models.fields import *
|
||||||
from django.db.models.fields.subclassing import SubfieldBase
|
from django.db.models.fields.subclassing import SubfieldBase
|
||||||
|
from django.db.models.fields.files import FileField, ImageField
|
||||||
from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel, TABULAR, STACKED
|
from django.db.models.fields.related import ForeignKey, OneToOneField, ManyToManyField, ManyToOneRel, ManyToManyRel, OneToOneRel, TABULAR, STACKED
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import types
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from itertools import izip
|
from itertools import izip
|
||||||
|
from warnings import warn
|
||||||
try:
|
try:
|
||||||
set
|
set
|
||||||
except NameError:
|
except NameError:
|
||||||
|
@ -12,7 +13,7 @@ import django.db.models.manipulators # Imported to register signal handler.
|
||||||
import django.db.models.manager # Ditto.
|
import django.db.models.manager # Ditto.
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
|
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError
|
||||||
from django.db.models.fields import AutoField, ImageField
|
from django.db.models.fields import AutoField
|
||||||
from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
|
from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
|
||||||
from django.db.models.query import delete_objects, Q, CollectedObjects
|
from django.db.models.query import delete_objects, Q, CollectedObjects
|
||||||
from django.db.models.options import Options
|
from django.db.models.options import Options
|
||||||
|
@ -463,110 +464,42 @@ class Model(object):
|
||||||
return getattr(self, cachename)
|
return getattr(self, cachename)
|
||||||
|
|
||||||
def _get_FIELD_filename(self, field):
|
def _get_FIELD_filename(self, field):
|
||||||
if getattr(self, field.attname): # Value is not blank.
|
warn("instance.get_%s_filename() is deprecated. Use instance.%s.path instead." % \
|
||||||
return os.path.normpath(os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname)))
|
(field.attname, field.attname), DeprecationWarning, stacklevel=3)
|
||||||
return ''
|
try:
|
||||||
|
return getattr(self, field.attname).path
|
||||||
|
except ValueError:
|
||||||
|
return ''
|
||||||
|
|
||||||
def _get_FIELD_url(self, field):
|
def _get_FIELD_url(self, field):
|
||||||
if getattr(self, field.attname): # Value is not blank.
|
warn("instance.get_%s_url() is deprecated. Use instance.%s.url instead." % \
|
||||||
import urlparse
|
(field.attname, field.attname), DeprecationWarning, stacklevel=3)
|
||||||
return urlparse.urljoin(settings.MEDIA_URL, getattr(self, field.attname)).replace('\\', '/')
|
try:
|
||||||
return ''
|
return getattr(self, field.attname).url
|
||||||
|
except ValueError:
|
||||||
|
return ''
|
||||||
|
|
||||||
def _get_FIELD_size(self, field):
|
def _get_FIELD_size(self, field):
|
||||||
return os.path.getsize(self._get_FIELD_filename(field))
|
warn("instance.get_%s_size() is deprecated. Use instance.%s.size instead." % \
|
||||||
|
(field.attname, field.attname), DeprecationWarning, stacklevel=3)
|
||||||
|
return getattr(self, field.attname).size
|
||||||
|
|
||||||
def _save_FIELD_file(self, field, filename, raw_field, save=True):
|
def _save_FIELD_file(self, field, filename, content, save=True):
|
||||||
# Create the upload directory if it doesn't already exist
|
warn("instance.save_%s_file() is deprecated. Use instance.%s.save() instead." % \
|
||||||
directory = os.path.join(settings.MEDIA_ROOT, field.get_directory_name())
|
(field.attname, field.attname), DeprecationWarning, stacklevel=3)
|
||||||
if not os.path.exists(directory):
|
return getattr(self, field.attname).save(filename, content, save)
|
||||||
os.makedirs(directory)
|
|
||||||
elif not os.path.isdir(directory):
|
|
||||||
raise IOError('%s exists and is not a directory' % directory)
|
|
||||||
|
|
||||||
# Check for old-style usage (files-as-dictionaries). Warn here first
|
|
||||||
# since there are multiple locations where we need to support both new
|
|
||||||
# and old usage.
|
|
||||||
if isinstance(raw_field, dict):
|
|
||||||
import warnings
|
|
||||||
warnings.warn(
|
|
||||||
message = "Representing uploaded files as dictionaries is deprecated. Use django.core.files.uploadedfile.SimpleUploadedFile instead.",
|
|
||||||
category = DeprecationWarning,
|
|
||||||
stacklevel = 2
|
|
||||||
)
|
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
||||||
raw_field = SimpleUploadedFile.from_dict(raw_field)
|
|
||||||
|
|
||||||
elif isinstance(raw_field, basestring):
|
|
||||||
import warnings
|
|
||||||
warnings.warn(
|
|
||||||
message = "Representing uploaded files as strings is deprecated. Use django.core.files.uploadedfile.SimpleUploadedFile instead.",
|
|
||||||
category = DeprecationWarning,
|
|
||||||
stacklevel = 2
|
|
||||||
)
|
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
||||||
raw_field = SimpleUploadedFile(filename, raw_field)
|
|
||||||
|
|
||||||
if filename is None:
|
|
||||||
filename = raw_field.file_name
|
|
||||||
|
|
||||||
filename = field.get_filename(filename)
|
|
||||||
|
|
||||||
# If the filename already exists, keep adding an underscore to the name
|
|
||||||
# of the file until the filename doesn't exist.
|
|
||||||
while os.path.exists(os.path.join(settings.MEDIA_ROOT, filename)):
|
|
||||||
try:
|
|
||||||
dot_index = filename.rindex('.')
|
|
||||||
except ValueError: # filename has no dot.
|
|
||||||
filename += '_'
|
|
||||||
else:
|
|
||||||
filename = filename[:dot_index] + '_' + filename[dot_index:]
|
|
||||||
|
|
||||||
# Save the file name on the object and write the file to disk.
|
|
||||||
setattr(self, field.attname, filename)
|
|
||||||
full_filename = self._get_FIELD_filename(field)
|
|
||||||
if hasattr(raw_field, 'temporary_file_path'):
|
|
||||||
# This file has a file path that we can move.
|
|
||||||
file_move_safe(raw_field.temporary_file_path(), full_filename)
|
|
||||||
raw_field.close()
|
|
||||||
else:
|
|
||||||
# This is a normal uploadedfile that we can stream.
|
|
||||||
fp = open(full_filename, 'wb')
|
|
||||||
locks.lock(fp, locks.LOCK_EX)
|
|
||||||
for chunk in raw_field.chunks():
|
|
||||||
fp.write(chunk)
|
|
||||||
locks.unlock(fp)
|
|
||||||
fp.close()
|
|
||||||
|
|
||||||
# Save the width and/or height, if applicable.
|
|
||||||
if isinstance(field, ImageField) and \
|
|
||||||
(field.width_field or field.height_field):
|
|
||||||
from django.utils.images import get_image_dimensions
|
|
||||||
width, height = get_image_dimensions(full_filename)
|
|
||||||
if field.width_field:
|
|
||||||
setattr(self, field.width_field, width)
|
|
||||||
if field.height_field:
|
|
||||||
setattr(self, field.height_field, height)
|
|
||||||
|
|
||||||
# Save the object because it has changed, unless save is False.
|
|
||||||
if save:
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
_save_FIELD_file.alters_data = True
|
_save_FIELD_file.alters_data = True
|
||||||
|
|
||||||
def _get_FIELD_width(self, field):
|
def _get_FIELD_width(self, field):
|
||||||
return self._get_image_dimensions(field)[0]
|
warn("instance.get_%s_width() is deprecated. Use instance.%s.width instead." % \
|
||||||
|
(field.attname, field.attname), DeprecationWarning, stacklevel=3)
|
||||||
|
return getattr(self, field.attname).width()
|
||||||
|
|
||||||
def _get_FIELD_height(self, field):
|
def _get_FIELD_height(self, field):
|
||||||
return self._get_image_dimensions(field)[1]
|
warn("instance.get_%s_height() is deprecated. Use instance.%s.height instead." % \
|
||||||
|
(field.attname, field.attname), DeprecationWarning, stacklevel=3)
|
||||||
def _get_image_dimensions(self, field):
|
return getattr(self, field.attname).height()
|
||||||
cachename = "__%s_dimensions_cache" % field.name
|
|
||||||
if not hasattr(self, cachename):
|
|
||||||
from django.utils.images import get_image_dimensions
|
|
||||||
filename = self._get_FIELD_filename(field)
|
|
||||||
setattr(self, cachename, get_image_dimensions(filename))
|
|
||||||
return getattr(self, cachename)
|
|
||||||
|
|
||||||
|
|
||||||
############################################
|
############################################
|
||||||
|
|
|
@ -10,6 +10,7 @@ except ImportError:
|
||||||
from django.db import connection, get_creation_module
|
from django.db import connection, get_creation_module
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.db.models.query_utils import QueryWrapper
|
from django.db.models.query_utils import QueryWrapper
|
||||||
|
from django.dispatch import dispatcher
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django import oldforms
|
from django import oldforms
|
||||||
|
@ -757,131 +758,6 @@ class EmailField(CharField):
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
return super(EmailField, self).formfield(**defaults)
|
return super(EmailField, self).formfield(**defaults)
|
||||||
|
|
||||||
class FileField(Field):
|
|
||||||
def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs):
|
|
||||||
self.upload_to = upload_to
|
|
||||||
kwargs['max_length'] = kwargs.get('max_length', 100)
|
|
||||||
Field.__init__(self, verbose_name, name, **kwargs)
|
|
||||||
|
|
||||||
def get_internal_type(self):
|
|
||||||
return "FileField"
|
|
||||||
|
|
||||||
def get_db_prep_value(self, value):
|
|
||||||
"Returns field's value prepared for saving into a database."
|
|
||||||
# Need to convert UploadedFile objects provided via a form to unicode for database insertion
|
|
||||||
if hasattr(value, 'name'):
|
|
||||||
return value.name
|
|
||||||
elif value is None:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return unicode(value)
|
|
||||||
|
|
||||||
def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
|
|
||||||
field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
|
|
||||||
if not self.blank:
|
|
||||||
if rel:
|
|
||||||
# This validator makes sure FileFields work in a related context.
|
|
||||||
class RequiredFileField(object):
|
|
||||||
def __init__(self, other_field_names, other_file_field_name):
|
|
||||||
self.other_field_names = other_field_names
|
|
||||||
self.other_file_field_name = other_file_field_name
|
|
||||||
self.always_test = True
|
|
||||||
def __call__(self, field_data, all_data):
|
|
||||||
if not all_data.get(self.other_file_field_name, False):
|
|
||||||
c = validators.RequiredIfOtherFieldsGiven(self.other_field_names, ugettext_lazy("This field is required."))
|
|
||||||
c(field_data, all_data)
|
|
||||||
# First, get the core fields, if any.
|
|
||||||
core_field_names = []
|
|
||||||
for f in opts.fields:
|
|
||||||
if f.core and f != self:
|
|
||||||
core_field_names.extend(f.get_manipulator_field_names(name_prefix))
|
|
||||||
# Now, if there are any, add the validator to this FormField.
|
|
||||||
if core_field_names:
|
|
||||||
field_list[0].validator_list.append(RequiredFileField(core_field_names, field_list[1].field_name))
|
|
||||||
else:
|
|
||||||
v = validators.RequiredIfOtherFieldNotGiven(field_list[1].field_name, ugettext_lazy("This field is required."))
|
|
||||||
v.always_test = True
|
|
||||||
field_list[0].validator_list.append(v)
|
|
||||||
field_list[0].is_required = field_list[1].is_required = False
|
|
||||||
|
|
||||||
# If the raw path is passed in, validate it's under the MEDIA_ROOT.
|
|
||||||
def isWithinMediaRoot(field_data, all_data):
|
|
||||||
f = os.path.abspath(os.path.join(settings.MEDIA_ROOT, field_data))
|
|
||||||
if not f.startswith(os.path.abspath(os.path.normpath(settings.MEDIA_ROOT))):
|
|
||||||
raise validators.ValidationError, _("Enter a valid filename.")
|
|
||||||
field_list[1].validator_list.append(isWithinMediaRoot)
|
|
||||||
return field_list
|
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
|
||||||
super(FileField, self).contribute_to_class(cls, name)
|
|
||||||
setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self))
|
|
||||||
setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self))
|
|
||||||
setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self))
|
|
||||||
setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_field, save=True: instance._save_FIELD_file(self, filename, raw_field, save))
|
|
||||||
signals.post_delete.connect(self.delete_file, sender=cls)
|
|
||||||
|
|
||||||
def delete_file(self, instance, **kwargs):
|
|
||||||
if getattr(instance, self.attname):
|
|
||||||
file_name = getattr(instance, 'get_%s_filename' % self.name)()
|
|
||||||
# If the file exists and no other object of this type references it,
|
|
||||||
# delete it from the filesystem.
|
|
||||||
if os.path.exists(file_name) and \
|
|
||||||
not instance.__class__._default_manager.filter(**{'%s__exact' % self.name: getattr(instance, self.attname)}):
|
|
||||||
os.remove(file_name)
|
|
||||||
|
|
||||||
def get_manipulator_field_objs(self):
|
|
||||||
return [oldforms.FileUploadField, oldforms.HiddenField]
|
|
||||||
|
|
||||||
def get_manipulator_field_names(self, name_prefix):
|
|
||||||
return [name_prefix + self.name + '_file', name_prefix + self.name]
|
|
||||||
|
|
||||||
def save_file(self, new_data, new_object, original_object, change, rel, save=True):
|
|
||||||
upload_field_name = self.get_manipulator_field_names('')[0]
|
|
||||||
if new_data.get(upload_field_name, False):
|
|
||||||
if rel:
|
|
||||||
file = new_data[upload_field_name][0]
|
|
||||||
else:
|
|
||||||
file = new_data[upload_field_name]
|
|
||||||
|
|
||||||
if not file:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Backwards-compatible support for files-as-dictionaries.
|
|
||||||
# We don't need to raise a warning because Model._save_FIELD_file will
|
|
||||||
# do so for us.
|
|
||||||
try:
|
|
||||||
file_name = file.name
|
|
||||||
except AttributeError:
|
|
||||||
file_name = file['filename']
|
|
||||||
|
|
||||||
func = getattr(new_object, 'save_%s_file' % self.name)
|
|
||||||
func(file_name, file, save)
|
|
||||||
|
|
||||||
def get_directory_name(self):
|
|
||||||
return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))
|
|
||||||
|
|
||||||
def get_filename(self, filename):
|
|
||||||
from django.utils.text import get_valid_filename
|
|
||||||
f = os.path.join(self.get_directory_name(), get_valid_filename(os.path.basename(filename)))
|
|
||||||
return os.path.normpath(f)
|
|
||||||
|
|
||||||
def save_form_data(self, instance, data):
|
|
||||||
from django.core.files.uploadedfile import UploadedFile
|
|
||||||
if data and isinstance(data, UploadedFile):
|
|
||||||
getattr(instance, "save_%s_file" % self.name)(data.name, data, save=False)
|
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
|
||||||
defaults = {'form_class': forms.FileField}
|
|
||||||
# If a file has been provided previously, then the form doesn't require
|
|
||||||
# that a new file is provided this time.
|
|
||||||
# The code to mark the form field as not required is used by
|
|
||||||
# form_for_instance, but can probably be removed once form_for_instance
|
|
||||||
# is gone. ModelForm uses a different method to check for an existing file.
|
|
||||||
if 'initial' in kwargs:
|
|
||||||
defaults['required'] = False
|
|
||||||
defaults.update(kwargs)
|
|
||||||
return super(FileField, self).formfield(**defaults)
|
|
||||||
|
|
||||||
class FilePathField(Field):
|
class FilePathField(Field):
|
||||||
def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
|
def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
|
||||||
self.path, self.match, self.recursive = path, match, recursive
|
self.path, self.match, self.recursive = path, match, recursive
|
||||||
|
@ -923,40 +799,6 @@ class FloatField(Field):
|
||||||
defaults.update(kwargs)
|
defaults.update(kwargs)
|
||||||
return super(FloatField, self).formfield(**defaults)
|
return super(FloatField, self).formfield(**defaults)
|
||||||
|
|
||||||
class ImageField(FileField):
|
|
||||||
def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs):
|
|
||||||
self.width_field, self.height_field = width_field, height_field
|
|
||||||
FileField.__init__(self, verbose_name, name, **kwargs)
|
|
||||||
|
|
||||||
def get_manipulator_field_objs(self):
|
|
||||||
return [oldforms.ImageUploadField, oldforms.HiddenField]
|
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
|
||||||
super(ImageField, self).contribute_to_class(cls, name)
|
|
||||||
# Add get_BLAH_width and get_BLAH_height methods, but only if the
|
|
||||||
# image field doesn't have width and height cache fields.
|
|
||||||
if not self.width_field:
|
|
||||||
setattr(cls, 'get_%s_width' % self.name, curry(cls._get_FIELD_width, field=self))
|
|
||||||
if not self.height_field:
|
|
||||||
setattr(cls, 'get_%s_height' % self.name, curry(cls._get_FIELD_height, field=self))
|
|
||||||
|
|
||||||
def save_file(self, new_data, new_object, original_object, change, rel, save=True):
|
|
||||||
FileField.save_file(self, new_data, new_object, original_object, change, rel, save)
|
|
||||||
# If the image has height and/or width field(s) and they haven't
|
|
||||||
# changed, set the width and/or height field(s) back to their original
|
|
||||||
# values.
|
|
||||||
if change and (self.width_field or self.height_field) and save:
|
|
||||||
if self.width_field:
|
|
||||||
setattr(new_object, self.width_field, getattr(original_object, self.width_field))
|
|
||||||
if self.height_field:
|
|
||||||
setattr(new_object, self.height_field, getattr(original_object, self.height_field))
|
|
||||||
new_object.save()
|
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
|
||||||
defaults = {'form_class': forms.ImageField}
|
|
||||||
defaults.update(kwargs)
|
|
||||||
return super(ImageField, self).formfield(**defaults)
|
|
||||||
|
|
||||||
class IntegerField(Field):
|
class IntegerField(Field):
|
||||||
empty_strings_allowed = False
|
empty_strings_allowed = False
|
||||||
def get_db_prep_value(self, value):
|
def get_db_prep_value(self, value):
|
||||||
|
|
|
@ -0,0 +1,315 @@
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models.fields import Field
|
||||||
|
from django.core.files.base import File, ContentFile
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.core.files.images import ImageFile, get_image_dimensions
|
||||||
|
from django.core.files.uploadedfile import UploadedFile
|
||||||
|
from django.utils.functional import curry
|
||||||
|
from django.db.models import signals
|
||||||
|
from django.utils.encoding import force_unicode, smart_str
|
||||||
|
from django.utils.translation import ugettext_lazy, ugettext as _
|
||||||
|
from django import oldforms
|
||||||
|
from django import forms
|
||||||
|
from django.core import validators
|
||||||
|
from django.db.models.loading import cache
|
||||||
|
|
||||||
|
class FieldFile(File):
|
||||||
|
def __init__(self, instance, field, name):
|
||||||
|
self.instance = instance
|
||||||
|
self.field = field
|
||||||
|
self.storage = field.storage
|
||||||
|
self._name = name or u''
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
# Older code may be expecting FileField values to be simple strings.
|
||||||
|
# By overriding the == operator, it can remain backwards compatibility.
|
||||||
|
if hasattr(other, 'name'):
|
||||||
|
return self.name == other.name
|
||||||
|
return self.name == other
|
||||||
|
|
||||||
|
# The standard File contains most of the necessary properties, but
|
||||||
|
# FieldFiles can be instantiated without a name, so that needs to
|
||||||
|
# be checked for here.
|
||||||
|
|
||||||
|
def _require_file(self):
|
||||||
|
if not self:
|
||||||
|
raise ValueError("The '%s' attribute has no file associated with it." % self.field.name)
|
||||||
|
|
||||||
|
def _get_file(self):
|
||||||
|
self._require_file()
|
||||||
|
if not hasattr(self, '_file'):
|
||||||
|
self._file = self.storage.open(self.name, 'rb')
|
||||||
|
return self._file
|
||||||
|
file = property(_get_file)
|
||||||
|
|
||||||
|
def _get_path(self):
|
||||||
|
self._require_file()
|
||||||
|
return self.storage.path(self.name)
|
||||||
|
path = property(_get_path)
|
||||||
|
|
||||||
|
def _get_url(self):
|
||||||
|
self._require_file()
|
||||||
|
return self.storage.url(self.name)
|
||||||
|
url = property(_get_url)
|
||||||
|
|
||||||
|
def open(self, mode='rb'):
|
||||||
|
self._require_file()
|
||||||
|
return super(FieldFile, self).open(mode)
|
||||||
|
# open() doesn't alter the file's contents, but it does reset the pointer
|
||||||
|
open.alters_data = True
|
||||||
|
|
||||||
|
# In addition to the standard File API, FieldFiles have extra methods
|
||||||
|
# to further manipulate the underlying file, as well as update the
|
||||||
|
# associated model instance.
|
||||||
|
|
||||||
|
def save(self, name, content, save=True):
|
||||||
|
name = self.field.generate_filename(self.instance, name)
|
||||||
|
self._name = self.storage.save(name, content)
|
||||||
|
setattr(self.instance, self.field.name, self.name)
|
||||||
|
|
||||||
|
# Update the filesize cache
|
||||||
|
self._size = len(content)
|
||||||
|
|
||||||
|
# Save the object because it has changed, unless save is False
|
||||||
|
if save:
|
||||||
|
self.instance.save()
|
||||||
|
save.alters_data = True
|
||||||
|
|
||||||
|
def delete(self, save=True):
|
||||||
|
self.close()
|
||||||
|
self.storage.delete(self.name)
|
||||||
|
|
||||||
|
self._name = None
|
||||||
|
setattr(self.instance, self.field.name, self.name)
|
||||||
|
|
||||||
|
# Delete the filesize cache
|
||||||
|
if hasattr(self, '_size'):
|
||||||
|
del self._size
|
||||||
|
|
||||||
|
if save:
|
||||||
|
self.instance.save()
|
||||||
|
delete.alters_data = True
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
# FieldFile needs access to its associated model field and an instance
|
||||||
|
# it's attached to in order to work properly, but the only necessary
|
||||||
|
# data to be pickled is the file's name itself. Everything else will
|
||||||
|
# be restored later, by FileDescriptor below.
|
||||||
|
return {'_name': self.name, '_closed': False}
|
||||||
|
|
||||||
|
class FileDescriptor(object):
|
||||||
|
def __init__(self, field):
|
||||||
|
self.field = field
|
||||||
|
|
||||||
|
def __get__(self, instance=None, owner=None):
|
||||||
|
if instance is None:
|
||||||
|
raise AttributeError, "%s can only be accessed from %s instances." % (self.field.name(self.owner.__name__))
|
||||||
|
file = instance.__dict__[self.field.name]
|
||||||
|
if not isinstance(file, FieldFile):
|
||||||
|
# Create a new instance of FieldFile, based on a given file name
|
||||||
|
instance.__dict__[self.field.name] = self.field.attr_class(instance, self.field, file)
|
||||||
|
elif not hasattr(file, 'field'):
|
||||||
|
# The FieldFile was pickled, so some attributes need to be reset.
|
||||||
|
file.instance = instance
|
||||||
|
file.field = self.field
|
||||||
|
file.storage = self.field.storage
|
||||||
|
return instance.__dict__[self.field.name]
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
instance.__dict__[self.field.name] = value
|
||||||
|
|
||||||
|
class FileField(Field):
|
||||||
|
attr_class = FieldFile
|
||||||
|
|
||||||
|
def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs):
|
||||||
|
for arg in ('core', 'primary_key', 'unique'):
|
||||||
|
if arg in kwargs:
|
||||||
|
raise TypeError("'%s' is not a valid argument for %s." % (arg, self.__class__))
|
||||||
|
|
||||||
|
self.storage = storage or default_storage
|
||||||
|
self.upload_to = upload_to
|
||||||
|
if callable(upload_to):
|
||||||
|
self.generate_filename = upload_to
|
||||||
|
|
||||||
|
kwargs['max_length'] = kwargs.get('max_length', 100)
|
||||||
|
super(FileField, self).__init__(verbose_name, name, **kwargs)
|
||||||
|
|
||||||
|
def get_internal_type(self):
|
||||||
|
return "FileField"
|
||||||
|
|
||||||
|
def get_db_prep_lookup(self, lookup_type, value):
|
||||||
|
if hasattr(value, 'name'):
|
||||||
|
value = value.name
|
||||||
|
return super(FileField, self).get_db_prep_lookup(lookup_type, value)
|
||||||
|
|
||||||
|
def get_db_prep_value(self, value):
|
||||||
|
"Returns field's value prepared for saving into a database."
|
||||||
|
# Need to convert File objects provided via a form to unicode for database insertion
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return unicode(value)
|
||||||
|
|
||||||
|
def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
|
||||||
|
field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel, follow)
|
||||||
|
if not self.blank:
|
||||||
|
if rel:
|
||||||
|
# This validator makes sure FileFields work in a related context.
|
||||||
|
class RequiredFileField(object):
|
||||||
|
def __init__(self, other_field_names, other_file_field_name):
|
||||||
|
self.other_field_names = other_field_names
|
||||||
|
self.other_file_field_name = other_file_field_name
|
||||||
|
self.always_test = True
|
||||||
|
def __call__(self, field_data, all_data):
|
||||||
|
if not all_data.get(self.other_file_field_name, False):
|
||||||
|
c = validators.RequiredIfOtherFieldsGiven(self.other_field_names, ugettext_lazy("This field is required."))
|
||||||
|
c(field_data, all_data)
|
||||||
|
# First, get the core fields, if any.
|
||||||
|
core_field_names = []
|
||||||
|
for f in opts.fields:
|
||||||
|
if f.core and f != self:
|
||||||
|
core_field_names.extend(f.get_manipulator_field_names(name_prefix))
|
||||||
|
# Now, if there are any, add the validator to this FormField.
|
||||||
|
if core_field_names:
|
||||||
|
field_list[0].validator_list.append(RequiredFileField(core_field_names, field_list[1].field_name))
|
||||||
|
else:
|
||||||
|
v = validators.RequiredIfOtherFieldNotGiven(field_list[1].field_name, ugettext_lazy("This field is required."))
|
||||||
|
v.always_test = True
|
||||||
|
field_list[0].validator_list.append(v)
|
||||||
|
field_list[0].is_required = field_list[1].is_required = False
|
||||||
|
|
||||||
|
# If the raw path is passed in, validate it's under the MEDIA_ROOT.
|
||||||
|
def isWithinMediaRoot(field_data, all_data):
|
||||||
|
f = os.path.abspath(os.path.join(settings.MEDIA_ROOT, field_data))
|
||||||
|
if not f.startswith(os.path.abspath(os.path.normpath(settings.MEDIA_ROOT))):
|
||||||
|
raise validators.ValidationError(_("Enter a valid filename."))
|
||||||
|
field_list[1].validator_list.append(isWithinMediaRoot)
|
||||||
|
return field_list
|
||||||
|
|
||||||
|
def contribute_to_class(self, cls, name):
|
||||||
|
super(FileField, self).contribute_to_class(cls, name)
|
||||||
|
setattr(cls, self.name, FileDescriptor(self))
|
||||||
|
setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self))
|
||||||
|
setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self))
|
||||||
|
setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self))
|
||||||
|
setattr(cls, 'save_%s_file' % self.name, lambda instance, name, content, save=True: instance._save_FIELD_file(self, name, content, save))
|
||||||
|
signals.post_delete.connect(self.delete_file, sender=cls)
|
||||||
|
|
||||||
|
def delete_file(self, instance, sender, **kwargs):
|
||||||
|
file = getattr(instance, self.attname)
|
||||||
|
# If no other object of this type references the file,
|
||||||
|
# and it's not the default value for future objects,
|
||||||
|
# delete it from the backend.
|
||||||
|
if file and file.name != self.default and \
|
||||||
|
not sender._default_manager.filter(**{self.name: file.name}):
|
||||||
|
file.delete(save=False)
|
||||||
|
elif file:
|
||||||
|
# Otherwise, just close the file, so it doesn't tie up resources.
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
def get_manipulator_field_objs(self):
|
||||||
|
return [oldforms.FileUploadField, oldforms.HiddenField]
|
||||||
|
|
||||||
|
def get_manipulator_field_names(self, name_prefix):
|
||||||
|
return [name_prefix + self.name + '_file', name_prefix + self.name]
|
||||||
|
|
||||||
|
def save_file(self, new_data, new_object, original_object, change, rel, save=True):
|
||||||
|
upload_field_name = self.get_manipulator_field_names('')[0]
|
||||||
|
if new_data.get(upload_field_name, False):
|
||||||
|
if rel:
|
||||||
|
file = new_data[upload_field_name][0]
|
||||||
|
else:
|
||||||
|
file = new_data[upload_field_name]
|
||||||
|
|
||||||
|
# Backwards-compatible support for files-as-dictionaries.
|
||||||
|
# We don't need to raise a warning because the storage backend will
|
||||||
|
# do so for us.
|
||||||
|
try:
|
||||||
|
filename = file.name
|
||||||
|
except AttributeError:
|
||||||
|
filename = file['filename']
|
||||||
|
filename = self.get_filename(filename)
|
||||||
|
|
||||||
|
getattr(new_object, self.attname).save(filename, file, save)
|
||||||
|
|
||||||
|
def get_directory_name(self):
|
||||||
|
return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to))))
|
||||||
|
|
||||||
|
def get_filename(self, filename):
|
||||||
|
return os.path.normpath(self.storage.get_valid_name(os.path.basename(filename)))
|
||||||
|
|
||||||
|
def generate_filename(self, instance, filename):
|
||||||
|
return os.path.join(self.get_directory_name(), self.get_filename(filename))
|
||||||
|
|
||||||
|
def save_form_data(self, instance, data):
|
||||||
|
if data and isinstance(data, UploadedFile):
|
||||||
|
getattr(instance, self.name).save(data.name, data, save=False)
|
||||||
|
|
||||||
|
def formfield(self, **kwargs):
|
||||||
|
defaults = {'form_class': forms.FileField}
|
||||||
|
# If a file has been provided previously, then the form doesn't require
|
||||||
|
# that a new file is provided this time.
|
||||||
|
# The code to mark the form field as not required is used by
|
||||||
|
# form_for_instance, but can probably be removed once form_for_instance
|
||||||
|
# is gone. ModelForm uses a different method to check for an existing file.
|
||||||
|
if 'initial' in kwargs:
|
||||||
|
defaults['required'] = False
|
||||||
|
defaults.update(kwargs)
|
||||||
|
return super(FileField, self).formfield(**defaults)
|
||||||
|
|
||||||
|
class ImageFieldFile(ImageFile, FieldFile):
|
||||||
|
def save(self, name, content, save=True):
|
||||||
|
|
||||||
|
if not hasattr(content, 'read'):
|
||||||
|
import warnings
|
||||||
|
warnings.warn(
|
||||||
|
message = "Representing files as strings is deprecated." \
|
||||||
|
"Use django.core.files.base.ContentFile instead.",
|
||||||
|
category = DeprecationWarning,
|
||||||
|
stacklevel = 2
|
||||||
|
)
|
||||||
|
content = ContentFile(content)
|
||||||
|
|
||||||
|
# Repopulate the image dimension cache.
|
||||||
|
self._dimensions_cache = get_image_dimensions(content)
|
||||||
|
|
||||||
|
# Update width/height fields, if needed
|
||||||
|
if self.field.width_field:
|
||||||
|
setattr(self.instance, self.field.width_field, self.width)
|
||||||
|
if self.field.height_field:
|
||||||
|
setattr(self.instance, self.field.height_field, self.height)
|
||||||
|
|
||||||
|
super(ImageFieldFile, self).save(name, content, save)
|
||||||
|
|
||||||
|
def delete(self, save=True):
|
||||||
|
# Clear the image dimensions cache
|
||||||
|
if hasattr(self, '_dimensions_cache'):
|
||||||
|
del self._dimensions_cache
|
||||||
|
super(ImageFieldFile, self).delete(save)
|
||||||
|
|
||||||
|
class ImageField(FileField):
|
||||||
|
attr_class = ImageFieldFile
|
||||||
|
|
||||||
|
def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs):
|
||||||
|
self.width_field, self.height_field = width_field, height_field
|
||||||
|
FileField.__init__(self, verbose_name, name, **kwargs)
|
||||||
|
|
||||||
|
def get_manipulator_field_objs(self):
|
||||||
|
return [oldforms.ImageUploadField, oldforms.HiddenField]
|
||||||
|
|
||||||
|
def contribute_to_class(self, cls, name):
|
||||||
|
super(ImageField, self).contribute_to_class(cls, name)
|
||||||
|
# Add get_BLAH_width and get_BLAH_height methods, but only if the
|
||||||
|
# image field doesn't have width and height cache fields.
|
||||||
|
if not self.width_field:
|
||||||
|
setattr(cls, 'get_%s_width' % self.name, curry(cls._get_FIELD_width, field=self))
|
||||||
|
if not self.height_field:
|
||||||
|
setattr(cls, 'get_%s_height' % self.name, curry(cls._get_FIELD_height, field=self))
|
||||||
|
|
||||||
|
def formfield(self, **kwargs):
|
||||||
|
defaults = {'form_class': forms.ImageField}
|
||||||
|
defaults.update(kwargs)
|
||||||
|
return super(ImageField, self).formfield(**defaults)
|
|
@ -1,7 +1,8 @@
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django import oldforms
|
from django import oldforms
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.db.models.fields import FileField, AutoField
|
from django.db.models.fields import AutoField
|
||||||
|
from django.db.models.fields.files import FileField
|
||||||
from django.db.models import signals
|
from django.db.models import signals
|
||||||
from django.utils.functional import curry
|
from django.utils.functional import curry
|
||||||
from django.utils.datastructures import DotExpandedDict
|
from django.utils.datastructures import DotExpandedDict
|
||||||
|
|
|
@ -1,22 +1,5 @@
|
||||||
"""
|
import warnings
|
||||||
Utility functions for handling images.
|
|
||||||
|
|
||||||
Requires PIL, as you might imagine.
|
from django.core.files.images import get_image_dimensions
|
||||||
"""
|
|
||||||
|
|
||||||
import ImageFile
|
warnings.warn("django.utils.images has been moved to django.core.files.images.", DeprecationWarning)
|
||||||
|
|
||||||
def get_image_dimensions(path):
|
|
||||||
"""Returns the (width, height) of an image at a given path."""
|
|
||||||
p = ImageFile.Parser()
|
|
||||||
fp = open(path, 'rb')
|
|
||||||
while 1:
|
|
||||||
data = fp.read(1024)
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
p.feed(data)
|
|
||||||
if p.image:
|
|
||||||
return p.image.size
|
|
||||||
break
|
|
||||||
fp.close()
|
|
||||||
return None
|
|
||||||
|
|
|
@ -596,3 +596,42 @@ smoothly:
|
||||||
instance, not a ``HandField``). So if your ``__unicode__()`` method
|
instance, not a ``HandField``). So if your ``__unicode__()`` method
|
||||||
automatically converts to the string form of your Python object, you can
|
automatically converts to the string form of your Python object, you can
|
||||||
save yourself a lot of work.
|
save yourself a lot of work.
|
||||||
|
|
||||||
|
Writing a ``FileField`` subclass
|
||||||
|
=================================
|
||||||
|
|
||||||
|
In addition to the above methods, fields that deal with files have a few other
|
||||||
|
special requirements which must be taken into account. The majority of the
|
||||||
|
mechanics provided by ``FileField``, such as controlling database storage and
|
||||||
|
retrieval, can remain unchanged, leaving subclasses to deal with the challenge
|
||||||
|
of supporting a particular type of file.
|
||||||
|
|
||||||
|
Django provides a ``File`` class, which is used as a proxy to the file's
|
||||||
|
contents and operations. This can be subclassed to customzie hwo the file is
|
||||||
|
accessed, and what methods are available. It lives at
|
||||||
|
``django.db.models.fields.files``, and its default behavior is explained in the
|
||||||
|
`file documentation`_.
|
||||||
|
|
||||||
|
Once a subclass of ``File`` is created, the new ``FileField`` subclass must be
|
||||||
|
told to use it. To do so, simply assign the new ``File`` subclass to the special
|
||||||
|
``attr_class`` attribute of the ``FileField`` subclass.
|
||||||
|
|
||||||
|
.. _file documentation: ../files/
|
||||||
|
|
||||||
|
A few suggestions
|
||||||
|
------------------
|
||||||
|
|
||||||
|
In addition to the above details, there are a few guidelines which can greatly
|
||||||
|
improve the efficiency and readability of the field's code.
|
||||||
|
|
||||||
|
1. The source for Django's own ``ImageField`` (in
|
||||||
|
``django/db/models/fields/files.py``) is a great example of how to
|
||||||
|
subclass ``FileField`` to support a particular type of file, as it
|
||||||
|
incorporates all of the techniques described above.
|
||||||
|
|
||||||
|
2. Cache file attributes wherever possible. Since files may be stored in
|
||||||
|
remote storage systems, retrieving them may cost extra time, or even
|
||||||
|
money, that isn't always necessary. Once a file is retrieved to obtain
|
||||||
|
some data about its content, cache as much of that data as possible to
|
||||||
|
reduce the number of times the file must be retrieved on subsequent
|
||||||
|
calls for that information.
|
||||||
|
|
|
@ -2298,53 +2298,34 @@ For a full example, see the `lookup API sample model`_.
|
||||||
get_FOO_filename()
|
get_FOO_filename()
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
For every ``FileField``, the object will have a ``get_FOO_filename()`` method,
|
**Deprecated in Django development version**; use ``object.FOO.name`` instead.
|
||||||
where ``FOO`` is the name of the field. This returns the full filesystem path
|
See `managing files`_ for details.
|
||||||
to the file, according to your ``MEDIA_ROOT`` setting.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
It is only valid to call this method **after** saving the model when the
|
|
||||||
field has been set. Prior to saving, the value returned will not contain
|
|
||||||
the upload directory (the `upload_to` parameter) in the path.
|
|
||||||
|
|
||||||
Note that ``ImageField`` is technically a subclass of ``FileField``, so every
|
|
||||||
model with an ``ImageField`` will also get this method.
|
|
||||||
|
|
||||||
get_FOO_url()
|
get_FOO_url()
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
For every ``FileField``, the object will have a ``get_FOO_url()`` method,
|
**Deprecated in Django development version**; use ``object.FOO.url`` instead.
|
||||||
where ``FOO`` is the name of the field. This returns the full URL to the file,
|
See `managing files`_ for details.
|
||||||
according to your ``MEDIA_URL`` setting. If the value is blank, this method
|
|
||||||
returns an empty string.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
As with ``get_FOO_filename()``, it is only valid to call this method
|
|
||||||
**after** saving the model, otherwise an incorrect result will be
|
|
||||||
returned.
|
|
||||||
|
|
||||||
get_FOO_size()
|
get_FOO_size()
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
For every ``FileField``, the object will have a ``get_FOO_size()`` method,
|
**Deprecated in Django development version**; use ``object.FOO.size`` instead.
|
||||||
where ``FOO`` is the name of the field. This returns the size of the file, in
|
See `managing files`_ for details.
|
||||||
bytes. (Behind the scenes, it uses ``os.path.getsize``.)
|
|
||||||
|
|
||||||
save_FOO_file(filename, raw_contents)
|
save_FOO_file(filename, raw_contents)
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
For every ``FileField``, the object will have a ``save_FOO_file()`` method,
|
**Deprecated in Django development version**; use ``object.FOO.save()`` instead.
|
||||||
where ``FOO`` is the name of the field. This saves the given file to the
|
See `managing files`_ for details.
|
||||||
filesystem, using the given filename. If a file with the given filename already
|
|
||||||
exists, Django adds an underscore to the end of the filename (but before the
|
|
||||||
extension) until the filename is available.
|
|
||||||
|
|
||||||
get_FOO_height() and get_FOO_width()
|
get_FOO_height() and get_FOO_width()
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
For every ``ImageField``, the object will have ``get_FOO_height()`` and
|
**Deprecated in Django development version**; use ``object.FOO.width`` and
|
||||||
``get_FOO_width()`` methods, where ``FOO`` is the name of the field. This
|
``object.FOO.height`` instead. See `managing files`_ for details.
|
||||||
returns the height (or width) of the image, as an integer, in pixels.
|
|
||||||
|
.. _`managing files`: ../files/
|
||||||
|
|
||||||
Shortcuts
|
Shortcuts
|
||||||
=========
|
=========
|
||||||
|
|
|
@ -0,0 +1,388 @@
|
||||||
|
==============
|
||||||
|
Managing files
|
||||||
|
==============
|
||||||
|
|
||||||
|
**New in Django development version**
|
||||||
|
|
||||||
|
This document describes Django's file access APIs.
|
||||||
|
|
||||||
|
By default, Django stores files locally, using the ``MEDIA_ROOT`` and
|
||||||
|
``MEDIA_URL`` settings_. The examples below assume that you're using
|
||||||
|
these defaults.
|
||||||
|
|
||||||
|
However, Django provides ways to write custom `file storage systems`_ that
|
||||||
|
allow you to completely customize where and how Django stores files. The
|
||||||
|
second half of this document describes how these storage systems work.
|
||||||
|
|
||||||
|
.. _file storage systems: `File storage`_
|
||||||
|
.. _settings: ../settings/
|
||||||
|
|
||||||
|
Using files in models
|
||||||
|
=====================
|
||||||
|
|
||||||
|
When you use a `FileField`_ or `ImageField`_, Django provides a set of APIs you can use to deal with that file.
|
||||||
|
|
||||||
|
.. _filefield: ../model-api/#filefield
|
||||||
|
.. _imagefield: ../model-api/#imagefield
|
||||||
|
|
||||||
|
Consider the following model, using a ``FileField`` to store a photo::
|
||||||
|
|
||||||
|
class Car(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
price = models.DecimalField(max_digits=5, decimal_places=2)
|
||||||
|
photo = models.ImageField(upload_to='cars')
|
||||||
|
|
||||||
|
Any ``Car`` instance will have a ``photo`` attribute that you can use to get at
|
||||||
|
the details of the attached photo::
|
||||||
|
|
||||||
|
>>> car = Car.object.get(name="57 Chevy")
|
||||||
|
>>> car.photo
|
||||||
|
<ImageFieldFile: chevy.jpg>
|
||||||
|
>>> car.photo.name
|
||||||
|
u'chevy.jpg'
|
||||||
|
>>> car.photo.path
|
||||||
|
u'/media/cars/chevy.jpg'
|
||||||
|
>>> car.photo.url
|
||||||
|
u'http://media.example.com/cars/chevy.jpg'
|
||||||
|
|
||||||
|
This object -- ``car.photo`` in the example -- is a ``File`` object, which means
|
||||||
|
it has all the methods and attributes described below.
|
||||||
|
|
||||||
|
The ``File`` object
|
||||||
|
===================
|
||||||
|
|
||||||
|
Internally, Django uses a ``django.core.files.File`` any time it needs to
|
||||||
|
represent a file. This object is a thin wrapper around Python's `built-in file
|
||||||
|
object`_ with some Django-specific additions.
|
||||||
|
|
||||||
|
.. _built-in file object: http://docs.python.org/lib/bltin-file-objects.html
|
||||||
|
|
||||||
|
Creating ``File`` instances
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
Most of the time you'll simply use a ``File`` that Django's given you (i.e. a
|
||||||
|
file attached to an model as above, or perhaps an `uploaded file`_).
|
||||||
|
|
||||||
|
.. _uploaded file: ../uploading_files/
|
||||||
|
|
||||||
|
If you need to construct a ``File`` yourself, the easiest way is to create one
|
||||||
|
using a Python built-in ``file`` object::
|
||||||
|
|
||||||
|
>>> from django.core.files import File
|
||||||
|
|
||||||
|
# Create a Python file object using open()
|
||||||
|
>>> f = open('/tmp/hello.world', 'w')
|
||||||
|
>>> myfile = File(f)
|
||||||
|
|
||||||
|
Now you can use any of the ``File`` attributes and methods defined below.
|
||||||
|
|
||||||
|
``File`` attributes and methods
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Django's ``File`` has the following attributes and methods:
|
||||||
|
|
||||||
|
``File.path``
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The absolute path to the file's location on a local filesystem.
|
||||||
|
|
||||||
|
Custom `file storage systems`_ may not store files locally; files stored on
|
||||||
|
these systems will have a ``path`` of ``None``.
|
||||||
|
|
||||||
|
``File.url``
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The URL where the file can be retrieved. This is often useful in templates_; for
|
||||||
|
example, a bit of a template for displaying a ``Car`` (see above) might look
|
||||||
|
like::
|
||||||
|
|
||||||
|
<img src='{{ car.photo.url }}' alt='{{ car.name }}' />
|
||||||
|
|
||||||
|
.. _templates: ../templates/
|
||||||
|
|
||||||
|
``File.size``
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The size of the file in bytes.
|
||||||
|
|
||||||
|
``File.open(mode=None)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Open or reopen the file (which by definition also does ``File.seek(0)``). The
|
||||||
|
``mode`` argument allows the same values as Python's standard ``open()``.
|
||||||
|
|
||||||
|
When reopening a file, ``mode`` will override whatever mode the file was
|
||||||
|
originally opened with; ``None`` means to reopen with the original mode.
|
||||||
|
|
||||||
|
``File.read(num_bytes=None)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Read content from the file. The optional ``size`` is the number of bytes to
|
||||||
|
read; if not specified, the file will be read to the end.
|
||||||
|
|
||||||
|
``File.__iter__()``
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Iterate over the file yielding one line at a time.
|
||||||
|
|
||||||
|
``File.chunks(chunk_size=None)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Iterate over the file yielding "chunks" of a given size. ``chunk_size`` defaults
|
||||||
|
to 64 KB.
|
||||||
|
|
||||||
|
This is especially useful with very large files since it allows them to be
|
||||||
|
streamed off disk and avoids storing the whole file in memory.
|
||||||
|
|
||||||
|
``File.multiple_chunks(chunk_size=None)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Returns ``True`` if the file is large enough to require multiple chunks to
|
||||||
|
access all of its content give some ``chunk_size``.
|
||||||
|
|
||||||
|
``File.write(content)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Writes the specified content string to the file. Depending on the storage system
|
||||||
|
behind the scenes, this content might not be fully committed until ``close()``
|
||||||
|
is called on the file.
|
||||||
|
|
||||||
|
``File.close()``
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Close the file.
|
||||||
|
|
||||||
|
.. TODO: document the rest of the File methods.
|
||||||
|
|
||||||
|
Additional ``ImageField`` attributes
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
``File.width`` and ``File.height``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
These attributes provide the dimensions of the image.
|
||||||
|
|
||||||
|
Additional methods on files attached to objects
|
||||||
|
-----------------------------------------------
|
||||||
|
|
||||||
|
Any ``File`` that's associated with an object (as with ``Car.photo``, above)
|
||||||
|
will also have a couple of extra methods:
|
||||||
|
|
||||||
|
``File.save(name, content, save=True)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Saves a new file with the file name and contents provided. This will not replace
|
||||||
|
the existing file, but will create a new file and update the object to point to
|
||||||
|
it. If ``save`` is ``True``, the model's ``save()`` method will be called once
|
||||||
|
the file is saved. That is, these two lines::
|
||||||
|
|
||||||
|
>>> car.photo.save('myphoto.jpg', contents, save=False)
|
||||||
|
>>> car.save()
|
||||||
|
|
||||||
|
are the same as this one line::
|
||||||
|
|
||||||
|
>>> car.photo.save('myphoto.jpg', contents, save=True)
|
||||||
|
|
||||||
|
``File.delete(save=True)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Remove the file from the model instance and delete the underlying file. The
|
||||||
|
``save`` argument works as above.
|
||||||
|
|
||||||
|
File storage
|
||||||
|
============
|
||||||
|
|
||||||
|
Behind the scenes, Django delegates decisions about how and where to store files
|
||||||
|
to a file storage system. This is the object that actually understands things
|
||||||
|
like file systems, opening and reading files, etc.
|
||||||
|
|
||||||
|
Django's default file storage is given by the `DEFAULT_FILE_STORAGE setting`_;
|
||||||
|
if you don't explicitly provide a storage system, this is the one that will be
|
||||||
|
used.
|
||||||
|
|
||||||
|
.. _default_file_storage setting: ../settings/#default-file-storage
|
||||||
|
|
||||||
|
The built-in filesystem storage class
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
Django ships with a built-in ``FileSystemStorage`` class (defined in
|
||||||
|
``django.core.files.storage``) which implements basic local filesystem file
|
||||||
|
storage. Its initializer takes two arguments:
|
||||||
|
|
||||||
|
====================== ===================================================
|
||||||
|
Argument Description
|
||||||
|
====================== ===================================================
|
||||||
|
``location`` Optional. Absolute path to the directory that will
|
||||||
|
hold the files. If omitted, it will be set to the
|
||||||
|
value of your ``MEDIA_ROOT`` setting.
|
||||||
|
``base_url`` Optional. URL that serves the files stored at this
|
||||||
|
location. If omitted, it will default to the value
|
||||||
|
of your ``MEDIA_URL`` setting.
|
||||||
|
====================== ===================================================
|
||||||
|
|
||||||
|
For example, the following code will store uploaded files under
|
||||||
|
``/media/photos`` regardless of what your ``MEDIA_ROOT`` setting is::
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
|
fs = FileSystemStorage(base_url='/media/photos')
|
||||||
|
|
||||||
|
class Car(models.Model):
|
||||||
|
...
|
||||||
|
photo = models.ImageField(storage=fs)
|
||||||
|
|
||||||
|
`Custom storage systems`_ work the same way: you can pass them in as the
|
||||||
|
``storage`` argument to a ``FileField``.
|
||||||
|
|
||||||
|
.. _custom storage systems: `writing a custom storage system`_
|
||||||
|
|
||||||
|
Storage objects
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Though most of the time you'll want to use a ``File`` object (which delegates to
|
||||||
|
the proper storage for that file), you can use file storage systems directly.
|
||||||
|
You can create an instance of some custom file storage class, or -- often more
|
||||||
|
useful -- you can use the global default storage system::
|
||||||
|
|
||||||
|
>>> from django.core.files.storage import default_storage
|
||||||
|
|
||||||
|
>>> path = default_storage.save('/path/to/file', 'new content')
|
||||||
|
>>> path
|
||||||
|
u'/path/to/file'
|
||||||
|
|
||||||
|
>>> default_storage.filesize(path)
|
||||||
|
11
|
||||||
|
>>> default_storage.open(path).read()
|
||||||
|
'new content'
|
||||||
|
|
||||||
|
>>> default_storage.delete(path)
|
||||||
|
>>> default_storage.exists(path)
|
||||||
|
False
|
||||||
|
|
||||||
|
Storage objects define the following methods:
|
||||||
|
|
||||||
|
``Storage.exists(name)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
``True`` if a file exists given some ``name``.
|
||||||
|
|
||||||
|
``Storge.path(name)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The local filesystem path where the file can be opened using Python's standard
|
||||||
|
``open()``. For storage systems that aren't accessible from the local
|
||||||
|
filesystem, this will raise ``NotImplementedError`` instead.
|
||||||
|
|
||||||
|
``Storage.size(name)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Returns the total size, in bytes, of the file referenced by ``name``.
|
||||||
|
|
||||||
|
``Storage.url(name)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Returns the URL where the contents of the file referenced by ``name`` can be
|
||||||
|
accessed.
|
||||||
|
|
||||||
|
``Storage.open(name, mode='rb')``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Opens the file given by ``name``. Note that although the returned file is
|
||||||
|
guaranteed to be a ``File`` object, it might actually be some subclass. In the
|
||||||
|
case of remote file storage this means that reading/writing could be quite slow,
|
||||||
|
so be warned.
|
||||||
|
|
||||||
|
``Storage.save(name, content)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Saves a new file using the storage system, preferably with the name specified.
|
||||||
|
If there already exists a file with this name ``name``, the storage system may
|
||||||
|
modify the filename as necessary to get a unique name. The actual name of the
|
||||||
|
stored file will be returned.
|
||||||
|
|
||||||
|
``Storage.delete(name)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Deletes the file referenced by ``name``. This method won't raise an exception if
|
||||||
|
the file doesn't exist.
|
||||||
|
|
||||||
|
Writing a custom storage system
|
||||||
|
===============================
|
||||||
|
|
||||||
|
If you need to provide custom file storage -- a common example is storing files
|
||||||
|
on some remote system -- you can do so by defining a custom storage class.
|
||||||
|
You'll need to follow these steps:
|
||||||
|
|
||||||
|
#. Your custom storage system must be a subclass of
|
||||||
|
``django.core.files.storage.Storage``::
|
||||||
|
|
||||||
|
from django.core.files.storage import Storage
|
||||||
|
|
||||||
|
class MyStorage(Storage):
|
||||||
|
...
|
||||||
|
|
||||||
|
#. Django must be able to instantiate your storage system without any arguments.
|
||||||
|
This means that any settings should be taken from ``django.conf.settings``::
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import Storage
|
||||||
|
|
||||||
|
class MyStorage(Storage):
|
||||||
|
def __init__(self, option=None):
|
||||||
|
if not option:
|
||||||
|
option = settings.CUSTOM_STORAGE_OPTIONS
|
||||||
|
...
|
||||||
|
|
||||||
|
#. Your storage class must implement the ``_open()`` and ``_save()`` methods,
|
||||||
|
along with any other methods appropriate to your storage class. See below for
|
||||||
|
more on these methods.
|
||||||
|
|
||||||
|
In addition, if your class provides local file storage, it must override
|
||||||
|
the ``path()`` method.
|
||||||
|
|
||||||
|
Custom storage system methods
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Your custom storage system may override any of the storage methods explained
|
||||||
|
above in `storage objects`_. However, it's usually better to use the hooks
|
||||||
|
specifically designed for custom storage objects. These are:
|
||||||
|
|
||||||
|
``_open(name, mode='rb')``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
**Required**.
|
||||||
|
|
||||||
|
Called by ``Storage.open()``, this is the actual mechanism the storage class
|
||||||
|
uses to open the file. This must return a ``File`` object, though in most cases,
|
||||||
|
you'll want to return some subclass here that implements logic specific to the
|
||||||
|
backend storage system.
|
||||||
|
|
||||||
|
``_save(name, content)``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Called by ``Storage.save()``. The ``name`` will already have gone through
|
||||||
|
``get_valid_name()`` and ``get_available_name()``, and the ``content`` will be a
|
||||||
|
``File`` object itself. No return value is expected.
|
||||||
|
|
||||||
|
``get_valid_name(name)``
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Returns a filename suitable for use with the underlying storage system. The
|
||||||
|
``name`` argument passed to this method is the original filename sent to the
|
||||||
|
server, after having any path information removed. Override this to customize
|
||||||
|
how non-standard characters are converted to safe filenames.
|
||||||
|
|
||||||
|
The code provided on ``Storage`` retains only alpha-numeric characters, periods
|
||||||
|
and underscores from the original filename, removing everything else.
|
||||||
|
|
||||||
|
``get_available_name(name)``
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Returns a filename that is available in the storage mechanism, possibly taking
|
||||||
|
the provided filename into account. The ``name`` argument passed to this method
|
||||||
|
will have already cleaned to a filename valid for the storage system, according
|
||||||
|
to the ``get_valid_name()`` method described above.
|
||||||
|
|
||||||
|
The code provided on ``Storage`` simply appends underscores to the filename
|
||||||
|
until it finds one that's available in the destination directory.
|
|
@ -224,26 +224,64 @@ set to 75 by default, but you can specify it to override default behavior.
|
||||||
``FileField``
|
``FileField``
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
A file-upload field. Has one **required** argument:
|
A file-upload field. Has two special arguments, of which the first is
|
||||||
|
**required**:
|
||||||
|
|
||||||
====================== ===================================================
|
====================== ===================================================
|
||||||
Argument Description
|
Argument Description
|
||||||
====================== ===================================================
|
====================== ===================================================
|
||||||
``upload_to`` A local filesystem path that will be appended to
|
``upload_to`` Required. A filesystem-style path that will be
|
||||||
your ``MEDIA_ROOT`` setting to determine the
|
prepended to the filename before being committed to
|
||||||
output of the ``get_<fieldname>_url()`` helper
|
the final storage destination.
|
||||||
function.
|
|
||||||
|
**New in Django development version**
|
||||||
|
|
||||||
|
This may also be a callable, such as a function,
|
||||||
|
which will be called to obtain the upload path,
|
||||||
|
including the filename. See below for details.
|
||||||
|
|
||||||
|
``storage`` **New in Django development version**
|
||||||
|
|
||||||
|
Optional. A storage object, which handles the
|
||||||
|
storage and retrieval of your files. See `managing
|
||||||
|
files`_ for details on how to provide this object.
|
||||||
====================== ===================================================
|
====================== ===================================================
|
||||||
|
|
||||||
This path may contain `strftime formatting`_, which will be replaced by the
|
.. _managing files: ../files/
|
||||||
date/time of the file upload (so that uploaded files don't fill up the given
|
|
||||||
directory).
|
The ``upload_to`` path may contain `strftime formatting`_, which will be
|
||||||
|
replaced by the date/time of the file upload (so that uploaded files don't fill
|
||||||
|
up the given directory).
|
||||||
|
|
||||||
|
**New in Django development version**
|
||||||
|
|
||||||
|
If a callable is provided for the ``upload_to`` argument, that callable must be
|
||||||
|
able to accept two arguments, and return a Unix-style path (with forward
|
||||||
|
slashes) to be passed along to the storage system. The two arguments that will
|
||||||
|
be passed are:
|
||||||
|
|
||||||
|
====================== ===================================================
|
||||||
|
Argument Description
|
||||||
|
====================== ===================================================
|
||||||
|
``instance`` An instance of the model where the ``FileField`` is
|
||||||
|
defined. More specifically, this is the particular
|
||||||
|
instance where the current file is being attached.
|
||||||
|
|
||||||
|
**Note**: In most cases, this object will not have
|
||||||
|
been saved to the database yet, so if it uses the
|
||||||
|
default ``AutoField``, *it might not yet have a
|
||||||
|
value for its primary key field*.
|
||||||
|
|
||||||
|
``filename`` The filename that was originally given to the file.
|
||||||
|
This may or may not be taken into account when
|
||||||
|
determining the final destination path.
|
||||||
|
====================== ===================================================
|
||||||
|
|
||||||
The admin represents this field as an ``<input type="file">`` (a file-upload
|
The admin represents this field as an ``<input type="file">`` (a file-upload
|
||||||
widget).
|
widget).
|
||||||
|
|
||||||
Using a ``FileField`` or an ``ImageField`` (see below) in a model takes a few
|
Using a ``FileField`` or an ``ImageField`` (see below) in a model without a
|
||||||
steps:
|
specified storage system takes a few steps:
|
||||||
|
|
||||||
1. In your settings file, you'll need to define ``MEDIA_ROOT`` as the
|
1. In your settings file, you'll need to define ``MEDIA_ROOT`` as the
|
||||||
full path to a directory where you'd like Django to store uploaded
|
full path to a directory where you'd like Django to store uploaded
|
||||||
|
|
|
@ -426,6 +426,16 @@ Default content type to use for all ``HttpResponse`` objects, if a MIME type
|
||||||
isn't manually specified. Used with ``DEFAULT_CHARSET`` to construct the
|
isn't manually specified. Used with ``DEFAULT_CHARSET`` to construct the
|
||||||
``Content-Type`` header.
|
``Content-Type`` header.
|
||||||
|
|
||||||
|
DEFAULT_FILE_STORAGE
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Default: ``'django.core.filestorage.filesystem.FileSystemStorage'``
|
||||||
|
|
||||||
|
Default file storage class to be used for any file-related operations that don't
|
||||||
|
specify a particular storage system. See the `file documentation`_ for details.
|
||||||
|
|
||||||
|
.. _file documentation: ../files/
|
||||||
|
|
||||||
DEFAULT_FROM_EMAIL
|
DEFAULT_FROM_EMAIL
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
@ -155,25 +155,8 @@ Three `settings`_ control Django's file upload behavior:
|
||||||
``UploadedFile`` objects
|
``UploadedFile`` objects
|
||||||
========================
|
========================
|
||||||
|
|
||||||
All ``UploadedFile`` objects define the following methods/attributes:
|
In addition to those inherited from `File`_, all ``UploadedFile`` objects define
|
||||||
|
the following methods/attributes:
|
||||||
``UploadedFile.read(self, num_bytes=None)``
|
|
||||||
Returns a byte string of length ``num_bytes``, or the complete file if
|
|
||||||
``num_bytes`` is ``None``.
|
|
||||||
|
|
||||||
``UploadedFile.chunks(self, chunk_size=None)``
|
|
||||||
A generator yielding small chunks from the file. If ``chunk_size`` isn't
|
|
||||||
given, chunks will be 64 KB.
|
|
||||||
|
|
||||||
``UploadedFile.multiple_chunks(self, chunk_size=None)``
|
|
||||||
Returns ``True`` if you can expect more than one chunk when calling
|
|
||||||
``UploadedFile.chunks(self, chunk_size)``.
|
|
||||||
|
|
||||||
``UploadedFile.size``
|
|
||||||
The size, in bytes, of the uploaded file.
|
|
||||||
|
|
||||||
``UploadedFile.name``
|
|
||||||
The name of the uploaded file as provided by the user.
|
|
||||||
|
|
||||||
``UploadedFile.content_type``
|
``UploadedFile.content_type``
|
||||||
The content-type header uploaded with the file (e.g. ``text/plain`` or
|
The content-type header uploaded with the file (e.g. ``text/plain`` or
|
||||||
|
@ -186,13 +169,11 @@ All ``UploadedFile`` objects define the following methods/attributes:
|
||||||
For ``text/*`` content-types, the character set (i.e. ``utf8``) supplied
|
For ``text/*`` content-types, the character set (i.e. ``utf8``) supplied
|
||||||
by the browser. Again, "trust but verify" is the best policy here.
|
by the browser. Again, "trust but verify" is the best policy here.
|
||||||
|
|
||||||
``UploadedFile.__iter__()``
|
|
||||||
Iterates over the lines in the file.
|
|
||||||
|
|
||||||
``UploadedFile.temporary_file_path()``
|
``UploadedFile.temporary_file_path()``
|
||||||
Only files uploaded onto disk will have this method; it returns the full
|
Only files uploaded onto disk will have this method; it returns the full
|
||||||
path to the temporary uploaded file.
|
path to the temporary uploaded file.
|
||||||
|
|
||||||
|
.. _File: ../files/
|
||||||
|
|
||||||
Upload Handlers
|
Upload Handlers
|
||||||
===============
|
===============
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
"""
|
||||||
|
42. Storing files according to a custom storage system
|
||||||
|
|
||||||
|
FileField and its variations can take a "storage" argument to specify how and
|
||||||
|
where files should be stored.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
temp_storage = FileSystemStorage(location=tempfile.gettempdir())
|
||||||
|
|
||||||
|
# Write out a file to be used as default content
|
||||||
|
temp_storage.save('tests/default.txt', ContentFile('default content'))
|
||||||
|
|
||||||
|
class Storage(models.Model):
|
||||||
|
def custom_upload_to(self, filename):
|
||||||
|
return 'foo'
|
||||||
|
|
||||||
|
def random_upload_to(self, filename):
|
||||||
|
# This returns a different result each time,
|
||||||
|
# to make sure it only gets called once.
|
||||||
|
import random
|
||||||
|
return '%s/%s' % (random.randint(100, 999), filename)
|
||||||
|
|
||||||
|
normal = models.FileField(storage=temp_storage, upload_to='tests')
|
||||||
|
custom = models.FileField(storage=temp_storage, upload_to=custom_upload_to)
|
||||||
|
random = models.FileField(storage=temp_storage, upload_to=random_upload_to)
|
||||||
|
default = models.FileField(storage=temp_storage, upload_to='tests', default='tests/default.txt')
|
||||||
|
|
||||||
|
__test__ = {'API_TESTS':"""
|
||||||
|
# An object without a file has limited functionality.
|
||||||
|
|
||||||
|
>>> obj1 = Storage()
|
||||||
|
>>> obj1.normal
|
||||||
|
<FieldFile: None>
|
||||||
|
>>> obj1.normal.size
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ValueError: The 'normal' attribute has no file associated with it.
|
||||||
|
|
||||||
|
# Saving a file enables full functionality.
|
||||||
|
|
||||||
|
>>> obj1.normal.save('django_test.txt', ContentFile('content'))
|
||||||
|
>>> obj1.normal
|
||||||
|
<FieldFile: tests/django_test.txt>
|
||||||
|
>>> obj1.normal.size
|
||||||
|
7
|
||||||
|
>>> obj1.normal.read()
|
||||||
|
'content'
|
||||||
|
|
||||||
|
# Files can be read in a little at a time, if necessary.
|
||||||
|
|
||||||
|
>>> obj1.normal.open()
|
||||||
|
>>> obj1.normal.read(3)
|
||||||
|
'con'
|
||||||
|
>>> obj1.normal.read()
|
||||||
|
'tent'
|
||||||
|
>>> '-'.join(obj1.normal.chunks(chunk_size=2))
|
||||||
|
'co-nt-en-t'
|
||||||
|
|
||||||
|
# Save another file with the same name.
|
||||||
|
|
||||||
|
>>> obj2 = Storage()
|
||||||
|
>>> obj2.normal.save('django_test.txt', ContentFile('more content'))
|
||||||
|
>>> obj2.normal
|
||||||
|
<FieldFile: tests/django_test_.txt>
|
||||||
|
>>> obj2.normal.size
|
||||||
|
12
|
||||||
|
|
||||||
|
# Push the objects into the cache to make sure they pickle properly
|
||||||
|
|
||||||
|
>>> cache.set('obj1', obj1)
|
||||||
|
>>> cache.set('obj2', obj2)
|
||||||
|
>>> cache.get('obj2').normal
|
||||||
|
<FieldFile: tests/django_test_.txt>
|
||||||
|
|
||||||
|
# Deleting an object deletes the file it uses, if there are no other objects
|
||||||
|
# still using that file.
|
||||||
|
|
||||||
|
>>> obj2.delete()
|
||||||
|
>>> obj2.normal.save('django_test.txt', ContentFile('more content'))
|
||||||
|
>>> obj2.normal
|
||||||
|
<FieldFile: tests/django_test_.txt>
|
||||||
|
|
||||||
|
# Default values allow an object to access a single file.
|
||||||
|
|
||||||
|
>>> obj3 = Storage.objects.create()
|
||||||
|
>>> obj3.default
|
||||||
|
<FieldFile: tests/default.txt>
|
||||||
|
>>> obj3.default.read()
|
||||||
|
'default content'
|
||||||
|
|
||||||
|
# But it shouldn't be deleted, even if there are no more objects using it.
|
||||||
|
|
||||||
|
>>> obj3.delete()
|
||||||
|
>>> obj3 = Storage()
|
||||||
|
>>> obj3.default.read()
|
||||||
|
'default content'
|
||||||
|
|
||||||
|
# Verify the fix for #5655, making sure the directory is only determined once.
|
||||||
|
|
||||||
|
>>> obj4 = Storage()
|
||||||
|
>>> obj4.random.save('random_file', ContentFile('random content'))
|
||||||
|
>>> obj4.random
|
||||||
|
<FieldFile: .../random_file>
|
||||||
|
|
||||||
|
# Clean up the temporary files.
|
||||||
|
|
||||||
|
>>> obj1.normal.delete()
|
||||||
|
>>> obj2.normal.delete()
|
||||||
|
>>> obj3.default.delete()
|
||||||
|
>>> obj4.random.delete()
|
||||||
|
"""}
|
|
@ -11,6 +11,9 @@ import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
|
temp_storage = FileSystemStorage(tempfile.gettempdir())
|
||||||
|
|
||||||
ARTICLE_STATUS = (
|
ARTICLE_STATUS = (
|
||||||
(1, 'Draft'),
|
(1, 'Draft'),
|
||||||
|
@ -60,7 +63,7 @@ class PhoneNumber(models.Model):
|
||||||
|
|
||||||
class TextFile(models.Model):
|
class TextFile(models.Model):
|
||||||
description = models.CharField(max_length=20)
|
description = models.CharField(max_length=20)
|
||||||
file = models.FileField(upload_to=tempfile.gettempdir())
|
file = models.FileField(storage=temp_storage, upload_to='tests')
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.description
|
return self.description
|
||||||
|
@ -73,9 +76,9 @@ class ImageFile(models.Model):
|
||||||
# for PyPy, you need to check for the underlying modules
|
# for PyPy, you need to check for the underlying modules
|
||||||
# If PIL is not available, this test is equivalent to TextFile above.
|
# If PIL is not available, this test is equivalent to TextFile above.
|
||||||
import Image, _imaging
|
import Image, _imaging
|
||||||
image = models.ImageField(upload_to=tempfile.gettempdir())
|
image = models.ImageField(storage=temp_storage, upload_to='tests')
|
||||||
except ImportError:
|
except ImportError:
|
||||||
image = models.FileField(upload_to=tempfile.gettempdir())
|
image = models.FileField(storage=temp_storage, upload_to='tests')
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.description
|
return self.description
|
||||||
|
@ -786,6 +789,8 @@ u'Assistance'
|
||||||
|
|
||||||
# FileField ###################################################################
|
# FileField ###################################################################
|
||||||
|
|
||||||
|
# File forms.
|
||||||
|
|
||||||
>>> class TextFileForm(ModelForm):
|
>>> class TextFileForm(ModelForm):
|
||||||
... class Meta:
|
... class Meta:
|
||||||
... model = TextFile
|
... model = TextFile
|
||||||
|
@ -808,9 +813,9 @@ True
|
||||||
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
|
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.file
|
>>> instance.file
|
||||||
u'...test1.txt'
|
<FieldFile: tests/test1.txt>
|
||||||
|
|
||||||
>>> os.unlink(instance.get_file_filename())
|
>>> instance.file.delete()
|
||||||
|
|
||||||
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test1.txt', 'hello world')})
|
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test1.txt', 'hello world')})
|
||||||
>>> f.is_valid()
|
>>> f.is_valid()
|
||||||
|
@ -819,7 +824,7 @@ True
|
||||||
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
|
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.file
|
>>> instance.file
|
||||||
u'...test1.txt'
|
<FieldFile: tests/test1.txt>
|
||||||
|
|
||||||
# Edit an instance that already has the file defined in the model. This will not
|
# Edit an instance that already has the file defined in the model. This will not
|
||||||
# save the file again, but leave it exactly as it is.
|
# save the file again, but leave it exactly as it is.
|
||||||
|
@ -828,13 +833,13 @@ u'...test1.txt'
|
||||||
>>> f.is_valid()
|
>>> f.is_valid()
|
||||||
True
|
True
|
||||||
>>> f.cleaned_data['file']
|
>>> f.cleaned_data['file']
|
||||||
u'...test1.txt'
|
<FieldFile: tests/test1.txt>
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.file
|
>>> instance.file
|
||||||
u'...test1.txt'
|
<FieldFile: tests/test1.txt>
|
||||||
|
|
||||||
# Delete the current file since this is not done by Django.
|
# Delete the current file since this is not done by Django.
|
||||||
>>> os.unlink(instance.get_file_filename())
|
>>> instance.file.delete()
|
||||||
|
|
||||||
# Override the file by uploading a new one.
|
# Override the file by uploading a new one.
|
||||||
|
|
||||||
|
@ -843,20 +848,20 @@ u'...test1.txt'
|
||||||
True
|
True
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.file
|
>>> instance.file
|
||||||
u'...test2.txt'
|
<FieldFile: tests/test2.txt>
|
||||||
|
|
||||||
# Delete the current file since this is not done by Django.
|
# Delete the current file since this is not done by Django.
|
||||||
>>> os.unlink(instance.get_file_filename())
|
>>> instance.file.delete()
|
||||||
|
|
||||||
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test2.txt', 'hello world')})
|
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test2.txt', 'hello world')})
|
||||||
>>> f.is_valid()
|
>>> f.is_valid()
|
||||||
True
|
True
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.file
|
>>> instance.file
|
||||||
u'...test2.txt'
|
<FieldFile: tests/test2.txt>
|
||||||
|
|
||||||
# Delete the current file since this is not done by Django.
|
# Delete the current file since this is not done by Django.
|
||||||
>>> os.unlink(instance.get_file_filename())
|
>>> instance.file.delete()
|
||||||
|
|
||||||
>>> instance.delete()
|
>>> instance.delete()
|
||||||
|
|
||||||
|
@ -868,17 +873,17 @@ u'...test2.txt'
|
||||||
True
|
True
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.file
|
>>> instance.file
|
||||||
''
|
<FieldFile: None>
|
||||||
|
|
||||||
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}, instance=instance)
|
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}, instance=instance)
|
||||||
>>> f.is_valid()
|
>>> f.is_valid()
|
||||||
True
|
True
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.file
|
>>> instance.file
|
||||||
u'...test3.txt'
|
<FieldFile: tests/test3.txt>
|
||||||
|
|
||||||
# Delete the current file since this is not done by Django.
|
# Delete the current file since this is not done by Django.
|
||||||
>>> os.unlink(instance.get_file_filename())
|
>>> instance.file.delete()
|
||||||
>>> instance.delete()
|
>>> instance.delete()
|
||||||
|
|
||||||
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')})
|
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')})
|
||||||
|
@ -886,10 +891,10 @@ u'...test3.txt'
|
||||||
True
|
True
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.file
|
>>> instance.file
|
||||||
u'...test3.txt'
|
<FieldFile: tests/test3.txt>
|
||||||
|
|
||||||
# Delete the current file since this is not done by Django.
|
# Delete the current file since this is not done by Django.
|
||||||
>>> os.unlink(instance.get_file_filename())
|
>>> instance.file.delete()
|
||||||
>>> instance.delete()
|
>>> instance.delete()
|
||||||
|
|
||||||
# ImageField ###################################################################
|
# ImageField ###################################################################
|
||||||
|
@ -911,10 +916,10 @@ True
|
||||||
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
|
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.image
|
>>> instance.image
|
||||||
u'...test.png'
|
<ImageFieldFile: tests/test.png>
|
||||||
|
|
||||||
# Delete the current file since this is not done by Django.
|
# Delete the current file since this is not done by Django.
|
||||||
>>> os.unlink(instance.get_image_filename())
|
>>> instance.image.delete()
|
||||||
|
|
||||||
>>> f = ImageFileForm(data={'description': u'An image'}, files={'image': SimpleUploadedFile('test.png', image_data)})
|
>>> f = ImageFileForm(data={'description': u'An image'}, files={'image': SimpleUploadedFile('test.png', image_data)})
|
||||||
>>> f.is_valid()
|
>>> f.is_valid()
|
||||||
|
@ -923,7 +928,7 @@ True
|
||||||
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
|
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.image
|
>>> instance.image
|
||||||
u'...test.png'
|
<ImageFieldFile: tests/test.png>
|
||||||
|
|
||||||
# Edit an instance that already has the image defined in the model. This will not
|
# Edit an instance that already has the image defined in the model. This will not
|
||||||
# save the image again, but leave it exactly as it is.
|
# save the image again, but leave it exactly as it is.
|
||||||
|
@ -932,14 +937,14 @@ u'...test.png'
|
||||||
>>> f.is_valid()
|
>>> f.is_valid()
|
||||||
True
|
True
|
||||||
>>> f.cleaned_data['image']
|
>>> f.cleaned_data['image']
|
||||||
u'...test.png'
|
<ImageFieldFile: tests/test.png>
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.image
|
>>> instance.image
|
||||||
u'...test.png'
|
<ImageFieldFile: tests/test.png>
|
||||||
|
|
||||||
# Delete the current image since this is not done by Django.
|
# Delete the current image since this is not done by Django.
|
||||||
|
|
||||||
>>> os.unlink(instance.get_image_filename())
|
>>> instance.image.delete()
|
||||||
|
|
||||||
# Override the file by uploading a new one.
|
# Override the file by uploading a new one.
|
||||||
|
|
||||||
|
@ -948,10 +953,10 @@ u'...test.png'
|
||||||
True
|
True
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.image
|
>>> instance.image
|
||||||
u'...test2.png'
|
<ImageFieldFile: tests/test2.png>
|
||||||
|
|
||||||
# Delete the current file since this is not done by Django.
|
# Delete the current file since this is not done by Django.
|
||||||
>>> os.unlink(instance.get_image_filename())
|
>>> instance.image.delete()
|
||||||
>>> instance.delete()
|
>>> instance.delete()
|
||||||
|
|
||||||
>>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': SimpleUploadedFile('test2.png', image_data)})
|
>>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': SimpleUploadedFile('test2.png', image_data)})
|
||||||
|
@ -959,10 +964,10 @@ u'...test2.png'
|
||||||
True
|
True
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.image
|
>>> instance.image
|
||||||
u'...test2.png'
|
<ImageFieldFile: tests/test2.png>
|
||||||
|
|
||||||
# Delete the current file since this is not done by Django.
|
# Delete the current file since this is not done by Django.
|
||||||
>>> os.unlink(instance.get_image_filename())
|
>>> instance.image.delete()
|
||||||
>>> instance.delete()
|
>>> instance.delete()
|
||||||
|
|
||||||
# Test the non-required ImageField
|
# Test the non-required ImageField
|
||||||
|
@ -973,17 +978,17 @@ u'...test2.png'
|
||||||
True
|
True
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.image
|
>>> instance.image
|
||||||
''
|
<ImageFieldFile: None>
|
||||||
|
|
||||||
>>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test3.png', image_data)}, instance=instance)
|
>>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test3.png', image_data)}, instance=instance)
|
||||||
>>> f.is_valid()
|
>>> f.is_valid()
|
||||||
True
|
True
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.image
|
>>> instance.image
|
||||||
u'...test3.png'
|
<ImageFieldFile: tests/test3.png>
|
||||||
|
|
||||||
# Delete the current file since this is not done by Django.
|
# Delete the current file since this is not done by Django.
|
||||||
>>> os.unlink(instance.get_image_filename())
|
>>> instance.image.delete()
|
||||||
>>> instance.delete()
|
>>> instance.delete()
|
||||||
|
|
||||||
>>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test3.png', image_data)})
|
>>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test3.png', image_data)})
|
||||||
|
@ -991,7 +996,7 @@ u'...test3.png'
|
||||||
True
|
True
|
||||||
>>> instance = f.save()
|
>>> instance = f.save()
|
||||||
>>> instance.image
|
>>> instance.image
|
||||||
u'...test3.png'
|
<ImageFieldFile: tests/test3.png>
|
||||||
>>> instance.delete()
|
>>> instance.delete()
|
||||||
|
|
||||||
# Media on a ModelForm ########################################################
|
# Media on a ModelForm ########################################################
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
|
||||||
class Member(models.Model):
|
class Member(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
@ -18,6 +19,7 @@ class Band(models.Model):
|
||||||
class Album(models.Model):
|
class Album(models.Model):
|
||||||
band = models.ForeignKey(Band)
|
band = models.ForeignKey(Band)
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
cover_art = models.ImageField(upload_to='albums')
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -46,12 +48,12 @@ HTML escaped.
|
||||||
>>> print conditional_escape(w.render('test', datetime(2007, 12, 1, 9, 30)))
|
>>> print conditional_escape(w.render('test', datetime(2007, 12, 1, 9, 30)))
|
||||||
<p class="datetime">Date: <input value="2007-12-01" type="text" class="vDateField" name="test_0" size="10" /><br />Time: <input value="09:30:00" type="text" class="vTimeField" name="test_1" size="8" /></p>
|
<p class="datetime">Date: <input value="2007-12-01" type="text" class="vDateField" name="test_0" size="10" /><br />Time: <input value="09:30:00" type="text" class="vTimeField" name="test_1" size="8" /></p>
|
||||||
|
|
||||||
>>> w = AdminFileWidget()
|
|
||||||
>>> print conditional_escape(w.render('test', 'test'))
|
|
||||||
Currently: <a target="_blank" href="%(MEDIA_URL)stest">test</a> <br />Change: <input type="file" name="test" />
|
|
||||||
|
|
||||||
>>> band = Band.objects.create(pk=1, name='Linkin Park')
|
>>> band = Band.objects.create(pk=1, name='Linkin Park')
|
||||||
>>> album = band.album_set.create(name='Hybrid Theory')
|
>>> album = band.album_set.create(name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg')
|
||||||
|
|
||||||
|
>>> w = AdminFileWidget()
|
||||||
|
>>> print conditional_escape(w.render('test', album.cover_art))
|
||||||
|
Currently: <a target="_blank" href="%(STORAGE_URL)salbums/hybrid_theory.jpg">albums\hybrid_theory.jpg</a> <br />Change: <input type="file" name="test" />
|
||||||
|
|
||||||
>>> rel = Album._meta.get_field('band').rel
|
>>> rel = Album._meta.get_field('band').rel
|
||||||
>>> w = ForeignKeyRawIdWidget(rel)
|
>>> w = ForeignKeyRawIdWidget(rel)
|
||||||
|
@ -81,5 +83,5 @@ True
|
||||||
|
|
||||||
""" % {
|
""" % {
|
||||||
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
|
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
|
||||||
'MEDIA_URL': settings.MEDIA_URL,
|
'STORAGE_URL': default_storage.url(''),
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
|
temp_storage = FileSystemStorage(tempfile.gettempdir())
|
||||||
|
|
||||||
class Photo(models.Model):
|
class Photo(models.Model):
|
||||||
title = models.CharField(max_length=30)
|
title = models.CharField(max_length=30)
|
||||||
image = models.FileField(upload_to=tempfile.gettempdir())
|
image = models.FileField(storage=temp_storage, upload_to='tests')
|
||||||
|
|
||||||
# Support code for the tests; this keeps track of how many times save() gets
|
# Support code for the tests; this keeps track of how many times save() gets
|
||||||
# called on each instance.
|
# called on each instance.
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(Photo, self).__init__(*args, **kwargs)
|
super(Photo, self).__init__(*args, **kwargs)
|
||||||
self._savecount = 0
|
self._savecount = 0
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
super(Photo, self).save()
|
super(Photo, self).save()
|
||||||
self._savecount +=1
|
self._savecount += 1
|
||||||
|
|
|
@ -36,4 +36,4 @@ class Bug639Test(unittest.TestCase):
|
||||||
Make sure to delete the "uploaded" file to avoid clogging /tmp.
|
Make sure to delete the "uploaded" file to avoid clogging /tmp.
|
||||||
"""
|
"""
|
||||||
p = Photo.objects.get()
|
p = Photo.objects.get()
|
||||||
os.unlink(p.get_image_filename())
|
p.image.delete(save=False)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from django.db import models
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
|
||||||
|
temp_storage = FileSystemStorage(tempfile.gettempdir())
|
||||||
|
|
||||||
|
# Test for correct behavior of width_field/height_field.
|
||||||
|
# Of course, we can't run this without PIL.
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Checking for the existence of Image is enough for CPython, but
|
||||||
|
# for PyPy, you need to check for the underlying modules
|
||||||
|
import Image, _imaging
|
||||||
|
except ImportError:
|
||||||
|
Image = None
|
||||||
|
|
||||||
|
# If we have PIL, do these tests
|
||||||
|
if Image:
|
||||||
|
class Person(models.Model):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
mugshot = models.ImageField(storage=temp_storage, upload_to='tests',
|
||||||
|
height_field='mug_height',
|
||||||
|
width_field='mug_width')
|
||||||
|
mug_height = models.PositiveSmallIntegerField()
|
||||||
|
mug_width = models.PositiveSmallIntegerField()
|
||||||
|
|
||||||
|
__test__ = {'API_TESTS': """
|
||||||
|
|
||||||
|
>>> image_data = open(os.path.join(os.path.dirname(__file__), "test.png"), 'rb').read()
|
||||||
|
>>> p = Person(name="Joe")
|
||||||
|
>>> p.mugshot.save("mug", ContentFile(image_data))
|
||||||
|
>>> p.mugshot.width
|
||||||
|
16
|
||||||
|
>>> p.mugshot.height
|
||||||
|
16
|
||||||
|
>>> p.mug_height
|
||||||
|
16
|
||||||
|
>>> p.mug_width
|
||||||
|
16
|
||||||
|
|
||||||
|
"""}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 482 B |
|
@ -0,0 +1,66 @@
|
||||||
|
"""
|
||||||
|
Tests for the file storage mechanism
|
||||||
|
|
||||||
|
>>> import tempfile
|
||||||
|
>>> from django.core.files.storage import FileSystemStorage
|
||||||
|
>>> from django.core.files.base import ContentFile
|
||||||
|
|
||||||
|
>>> temp_storage = FileSystemStorage(location=tempfile.gettempdir())
|
||||||
|
|
||||||
|
# Standard file access options are available, and work as expected.
|
||||||
|
|
||||||
|
>>> temp_storage.exists('storage_test')
|
||||||
|
False
|
||||||
|
>>> file = temp_storage.open('storage_test', 'w')
|
||||||
|
>>> file.write('storage contents')
|
||||||
|
>>> file.close()
|
||||||
|
|
||||||
|
>>> temp_storage.exists('storage_test')
|
||||||
|
True
|
||||||
|
>>> file = temp_storage.open('storage_test', 'r')
|
||||||
|
>>> file.read()
|
||||||
|
'storage contents'
|
||||||
|
>>> file.close()
|
||||||
|
|
||||||
|
>>> temp_storage.delete('storage_test')
|
||||||
|
>>> temp_storage.exists('storage_test')
|
||||||
|
False
|
||||||
|
|
||||||
|
# Files can only be accessed if they're below the specified location.
|
||||||
|
|
||||||
|
>>> temp_storage.exists('..')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
SuspiciousOperation: Attempted access to '..' denied.
|
||||||
|
>>> temp_storage.open('/etc/passwd')
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
SuspiciousOperation: Attempted access to '/etc/passwd' denied.
|
||||||
|
|
||||||
|
# Custom storage systems can be created to customize behavior
|
||||||
|
|
||||||
|
>>> class CustomStorage(FileSystemStorage):
|
||||||
|
... def get_available_name(self, name):
|
||||||
|
... # Append numbers to duplicate files rather than underscores, like Trac
|
||||||
|
...
|
||||||
|
... parts = name.split('.')
|
||||||
|
... basename, ext = parts[0], parts[1:]
|
||||||
|
... number = 2
|
||||||
|
...
|
||||||
|
... while self.exists(name):
|
||||||
|
... name = '.'.join([basename, str(number)] + ext)
|
||||||
|
... number += 1
|
||||||
|
...
|
||||||
|
... return name
|
||||||
|
>>> custom_storage = CustomStorage(tempfile.gettempdir())
|
||||||
|
|
||||||
|
>>> first = custom_storage.save('custom_storage', ContentFile('custom contents'))
|
||||||
|
>>> first
|
||||||
|
u'custom_storage'
|
||||||
|
>>> second = custom_storage.save('custom_storage', ContentFile('more contents'))
|
||||||
|
>>> second
|
||||||
|
u'custom_storage.2'
|
||||||
|
|
||||||
|
>>> custom_storage.delete(first)
|
||||||
|
>>> custom_storage.delete(second)
|
||||||
|
"""
|
|
@ -1,9 +1,10 @@
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
UPLOAD_ROOT = tempfile.mkdtemp()
|
temp_storage = FileSystemStorage(tempfile.mkdtemp())
|
||||||
UPLOAD_TO = os.path.join(UPLOAD_ROOT, 'test_upload')
|
UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload')
|
||||||
|
|
||||||
class FileModel(models.Model):
|
class FileModel(models.Model):
|
||||||
testfile = models.FileField(upload_to=UPLOAD_TO)
|
testfile = models.FileField(storage=temp_storage, upload_to='test_upload')
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.test import TestCase, client
|
||||||
from django.utils import simplejson
|
from django.utils import simplejson
|
||||||
from django.utils.hashcompat import sha_constructor
|
from django.utils.hashcompat import sha_constructor
|
||||||
|
|
||||||
from models import FileModel, UPLOAD_ROOT, UPLOAD_TO
|
from models import FileModel, temp_storage, UPLOAD_TO
|
||||||
|
|
||||||
class FileUploadTests(TestCase):
|
class FileUploadTests(TestCase):
|
||||||
def test_simple_upload(self):
|
def test_simple_upload(self):
|
||||||
|
@ -194,22 +194,22 @@ class DirectoryCreationTests(unittest.TestCase):
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.obj = FileModel()
|
self.obj = FileModel()
|
||||||
if not os.path.isdir(UPLOAD_ROOT):
|
if not os.path.isdir(temp_storage.location):
|
||||||
os.makedirs(UPLOAD_ROOT)
|
os.makedirs(temp_storage.location)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
os.chmod(UPLOAD_ROOT, 0700)
|
os.chmod(temp_storage.location, 0700)
|
||||||
shutil.rmtree(UPLOAD_ROOT)
|
shutil.rmtree(temp_storage.location)
|
||||||
|
|
||||||
def test_readonly_root(self):
|
def test_readonly_root(self):
|
||||||
"""Permission errors are not swallowed"""
|
"""Permission errors are not swallowed"""
|
||||||
os.chmod(UPLOAD_ROOT, 0500)
|
os.chmod(temp_storage.location, 0500)
|
||||||
try:
|
try:
|
||||||
self.obj.save_testfile_file('foo.txt', SimpleUploadedFile('foo.txt', 'x'))
|
self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', 'x'))
|
||||||
except OSError, err:
|
except OSError, err:
|
||||||
self.assertEquals(err.errno, errno.EACCES)
|
self.assertEquals(err.errno, errno.EACCES)
|
||||||
except:
|
except Exception, err:
|
||||||
self.fail("OSError [Errno %s] not raised" % errno.EACCES)
|
self.fail("OSError [Errno %s] not raised." % errno.EACCES)
|
||||||
|
|
||||||
def test_not_a_directory(self):
|
def test_not_a_directory(self):
|
||||||
"""The correct IOError is raised when the upload directory name exists but isn't a directory"""
|
"""The correct IOError is raised when the upload directory name exists but isn't a directory"""
|
||||||
|
@ -217,11 +217,11 @@ class DirectoryCreationTests(unittest.TestCase):
|
||||||
fd = open(UPLOAD_TO, 'w')
|
fd = open(UPLOAD_TO, 'w')
|
||||||
fd.close()
|
fd.close()
|
||||||
try:
|
try:
|
||||||
self.obj.save_testfile_file('foo.txt', SimpleUploadedFile('foo.txt', 'x'))
|
self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', 'x'))
|
||||||
except IOError, err:
|
except IOError, err:
|
||||||
# The test needs to be done on a specific string as IOError
|
# The test needs to be done on a specific string as IOError
|
||||||
# is raised even without the patch (just not early enough)
|
# is raised even without the patch (just not early enough)
|
||||||
self.assertEquals(err.args[0],
|
self.assertEquals(err.args[0],
|
||||||
"%s exists and is not a directory" % UPLOAD_TO)
|
"%s exists and is not a directory." % UPLOAD_TO)
|
||||||
except:
|
except:
|
||||||
self.fail("IOError not raised")
|
self.fail("IOError not raised")
|
||||||
|
|
|
@ -157,8 +157,8 @@ class DecimalPKData(models.Model):
|
||||||
class EmailPKData(models.Model):
|
class EmailPKData(models.Model):
|
||||||
data = models.EmailField(primary_key=True)
|
data = models.EmailField(primary_key=True)
|
||||||
|
|
||||||
class FilePKData(models.Model):
|
# class FilePKData(models.Model):
|
||||||
data = models.FileField(primary_key=True, upload_to='/foo/bar')
|
# data = models.FileField(primary_key=True, upload_to='/foo/bar')
|
||||||
|
|
||||||
class FilePathPKData(models.Model):
|
class FilePathPKData(models.Model):
|
||||||
data = models.FilePathField(primary_key=True)
|
data = models.FilePathField(primary_key=True)
|
||||||
|
|
|
@ -144,7 +144,7 @@ test_data = [
|
||||||
(data_obj, 41, EmailData, None),
|
(data_obj, 41, EmailData, None),
|
||||||
(data_obj, 42, EmailData, ""),
|
(data_obj, 42, EmailData, ""),
|
||||||
(data_obj, 50, FileData, 'file:///foo/bar/whiz.txt'),
|
(data_obj, 50, FileData, 'file:///foo/bar/whiz.txt'),
|
||||||
(data_obj, 51, FileData, None),
|
# (data_obj, 51, FileData, None),
|
||||||
(data_obj, 52, FileData, ""),
|
(data_obj, 52, FileData, ""),
|
||||||
(data_obj, 60, FilePathData, "/foo/bar/whiz.txt"),
|
(data_obj, 60, FilePathData, "/foo/bar/whiz.txt"),
|
||||||
(data_obj, 61, FilePathData, None),
|
(data_obj, 61, FilePathData, None),
|
||||||
|
@ -242,7 +242,7 @@ The end."""),
|
||||||
# (pk_obj, 620, DatePKData, datetime.date(2006,6,16)),
|
# (pk_obj, 620, DatePKData, datetime.date(2006,6,16)),
|
||||||
# (pk_obj, 630, DateTimePKData, datetime.datetime(2006,6,16,10,42,37)),
|
# (pk_obj, 630, DateTimePKData, datetime.datetime(2006,6,16,10,42,37)),
|
||||||
(pk_obj, 640, EmailPKData, "hovercraft@example.com"),
|
(pk_obj, 640, EmailPKData, "hovercraft@example.com"),
|
||||||
(pk_obj, 650, FilePKData, 'file:///foo/bar/whiz.txt'),
|
# (pk_obj, 650, FilePKData, 'file:///foo/bar/whiz.txt'),
|
||||||
(pk_obj, 660, FilePathPKData, "/foo/bar/whiz.txt"),
|
(pk_obj, 660, FilePathPKData, "/foo/bar/whiz.txt"),
|
||||||
(pk_obj, 670, DecimalPKData, decimal.Decimal('12.345')),
|
(pk_obj, 670, DecimalPKData, decimal.Decimal('12.345')),
|
||||||
(pk_obj, 671, DecimalPKData, decimal.Decimal('-12.345')),
|
(pk_obj, 671, DecimalPKData, decimal.Decimal('-12.345')),
|
||||||
|
|
Loading…
Reference in New Issue