Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

File storage refactoring, adding far more flexibility to Django's fil…

…e 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
  • Loading branch information...
commit 7899568e01fc9c68afe995fa71de915dd9fcdd76 1 parent c49eac7
@jacobian jacobian authored
Showing with 1,586 additions and 458 deletions.
  1. +3 −0  django/conf/global_settings.py
  2. +2 −2 django/contrib/admin/widgets.py
  3. +1 −0  django/core/files/__init__.py
  4. +169 −0 django/core/files/base.py
  5. +42 −0 django/core/files/images.py
  6. +214 −0 django/core/files/storage.py
  7. +2 −57 django/core/files/uploadedfile.py
  8. +1 −0  django/db/models/__init__.py
  9. +28 −95 django/db/models/base.py
  10. +1 −159 django/db/models/fields/__init__.py
  11. +315 −0 django/db/models/fields/files.py
  12. +2 −1  django/db/models/manipulators.py
  13. +3 −20 django/utils/images.py
  14. +39 −0 docs/custom_model_fields.txt
  15. +12 −31 docs/db-api.txt
  16. +388 −0 docs/files.txt
  17. +48 −10 docs/model-api.txt
  18. +10 −0 docs/settings.txt
  19. +3 −22 docs/upload_handling.txt
  20. +1 −0  tests/modeltests/files/__init__.py
  21. +118 −0 tests/modeltests/files/models.py
  22. +37 −32 tests/modeltests/model_forms/models.py
  23. +8 −6 tests/regressiontests/admin_widgets/models.py
  24. +8 −4 tests/regressiontests/bug639/models.py
  25. +1 −1  tests/regressiontests/bug639/tests.py
  26. +1 −0  tests/regressiontests/file_storage/__init__.py
  27. +44 −0 tests/regressiontests/file_storage/models.py
  28. BIN  tests/regressiontests/file_storage/test.png
  29. +66 −0 tests/regressiontests/file_storage/tests.py
  30. +4 −3 tests/regressiontests/file_uploads/models.py
  31. +11 −11 tests/regressiontests/file_uploads/tests.py
  32. +2 −2 tests/regressiontests/serializers_regress/models.py
  33. +2 −2 tests/regressiontests/serializers_regress/tests.py
View
3  django/conf/global_settings.py
@@ -226,6 +226,9 @@
# Path to the "jing" executable -- needed to validate XMLFields
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.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = ''
View
4 django/contrib/admin/widgets.py
@@ -85,8 +85,8 @@ def __init__(self, attrs={}):
def render(self, name, value, attrs=None):
output = []
if value:
- output.append('%s <a target="_blank" href="%s%s">%s</a> <br />%s ' % \
- (_('Currently:'), settings.MEDIA_URL, value, value, _('Change:')))
+ output.append('%s <a target="_blank" href="%s">%s</a> <br />%s ' % \
+ (_('Currently:'), value.url, value, _('Change:')))
output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(u''.join(output))
View
1  django/core/files/__init__.py
@@ -0,0 +1 @@
+from django.core.files.base import File
View
169 django/core/files/base.py
@@ -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)
View
42 django/core/files/images.py
@@ -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
View
214 django/core/files/storage.py
@@ -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()
View
59 django/core/files/uploadedfile.py
@@ -10,6 +10,7 @@
from StringIO import StringIO
from django.conf import settings
+from django.core.files.base import File
from django.core.files import temp as tempfile
@@ -39,7 +40,7 @@ def setter(self, value):
else:
return property(getter, setter)
-class UploadedFile(object):
+class UploadedFile(File):
"""
A abstract uploaded file (``TemporaryUploadedFile`` and
``InMemoryUploadedFile`` are the built-in concrete subclasses).
@@ -76,23 +77,6 @@ def _set_name(self, 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
filename = deprecated_property(old="filename", new="name")
file_name = deprecated_property(old="file_name", new="name")
@@ -108,18 +92,6 @@ def _get_data(self):
return self.read()
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
# define open/close.
def read(self, num_bytes=None):
@@ -131,33 +103,6 @@ def open(self):
def close(self):
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.
def __getitem__(self, key):
warnings.warn(
View
1  django/db/models/__init__.py
@@ -8,6 +8,7 @@
from django.db.models.base import Model
from django.db.models.fields import *
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 import signals
View
123 django/db/models/base.py
@@ -3,6 +3,7 @@
import sys
import os
from itertools import izip
+from warnings import warn
try:
set
except NameError:
@@ -12,7 +13,7 @@
import django.db.models.manager # Ditto.
from django.core import validators
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.query import delete_objects, Q, CollectedObjects
from django.db.models.options import Options
@@ -463,110 +464,42 @@ def _get_next_or_previous_in_order(self, is_next):
return getattr(self, cachename)
def _get_FIELD_filename(self, field):
- if getattr(self, field.attname): # Value is not blank.
- return os.path.normpath(os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname)))
- return ''
+ warn("instance.get_%s_filename() is deprecated. Use instance.%s.path instead." % \
+ (field.attname, field.attname), DeprecationWarning, stacklevel=3)
+ try:
+ return getattr(self, field.attname).path
+ except ValueError:
+ return ''
def _get_FIELD_url(self, field):
- if getattr(self, field.attname): # Value is not blank.
- import urlparse
- return urlparse.urljoin(settings.MEDIA_URL, getattr(self, field.attname)).replace('\\', '/')
- return ''
+ warn("instance.get_%s_url() is deprecated. Use instance.%s.url instead." % \
+ (field.attname, field.attname), DeprecationWarning, stacklevel=3)
+ try:
+ return getattr(self, field.attname).url
+ except ValueError:
+ return ''
def _get_FIELD_size(self, field):
- return os.path.getsize(self._get_FIELD_filename(field))
-
- def _save_FIELD_file(self, field, filename, raw_field, save=True):
- # Create the upload directory if it doesn't already exist
- directory = os.path.join(settings.MEDIA_ROOT, field.get_directory_name())
- 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)
-
- # 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()
+ 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, content, save=True):
+ warn("instance.save_%s_file() is deprecated. Use instance.%s.save() instead." % \
+ (field.attname, field.attname), DeprecationWarning, stacklevel=3)
+ return getattr(self, field.attname).save(filename, content, save)
_save_FIELD_file.alters_data = True
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):
- return self._get_image_dimensions(field)[1]
-
- def _get_image_dimensions(self, field):
- 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)
+ warn("instance.get_%s_height() is deprecated. Use instance.%s.height instead." % \
+ (field.attname, field.attname), DeprecationWarning, stacklevel=3)
+ return getattr(self, field.attname).height()
############################################
View
160 django/db/models/fields/__init__.py
@@ -10,6 +10,7 @@
from django.db import connection, get_creation_module
from django.db.models import signals
from django.db.models.query_utils import QueryWrapper
+from django.dispatch import dispatcher
from django.conf import settings
from django.core import validators
from django import oldforms
@@ -757,131 +758,6 @@ def formfield(self, **kwargs):
defaults.update(kwargs)
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):
def __init__(self, verbose_name=None, name=None, path='', match=None, recursive=False, **kwargs):
self.path, self.match, self.recursive = path, match, recursive
@@ -923,40 +799,6 @@ def formfield(self, **kwargs):
defaults.update(kwargs)
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):
empty_strings_allowed = False
def get_db_prep_value(self, value):
View
315 django/db/models/fields/files.py
@@ -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)
View
3  django/db/models/manipulators.py
@@ -1,7 +1,8 @@
from django.core.exceptions import ObjectDoesNotExist
from django import oldforms
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.utils.functional import curry
from django.utils.datastructures import DotExpandedDict
View
23 django/utils/images.py
@@ -1,22 +1,5 @@
-"""
-Utility functions for handling images.
+import warnings
-Requires PIL, as you might imagine.
-"""
+from django.core.files.images import get_image_dimensions
-import ImageFile
-
-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
+warnings.warn("django.utils.images has been moved to django.core.files.images.", DeprecationWarning)
View
39 docs/custom_model_fields.txt
@@ -596,3 +596,42 @@ smoothly:
instance, not a ``HandField``). So if your ``__unicode__()`` method
automatically converts to the string form of your Python object, you can
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.
View
43 docs/db-api.txt
@@ -2298,53 +2298,34 @@ For a full example, see the `lookup API sample model`_.
get_FOO_filename()
------------------
-For every ``FileField``, the object will have a ``get_FOO_filename()`` method,
-where ``FOO`` is the name of the field. This returns the full filesystem path
-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.
+**Deprecated in Django development version**; use ``object.FOO.name`` instead.
+See `managing files`_ for details.
get_FOO_url()
-------------
-For every ``FileField``, the object will have a ``get_FOO_url()`` method,
-where ``FOO`` is the name of the field. This returns the full URL to the file,
-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.
+**Deprecated in Django development version**; use ``object.FOO.url`` instead.
+See `managing files`_ for details.
get_FOO_size()
--------------
-For every ``FileField``, the object will have a ``get_FOO_size()`` method,
-where ``FOO`` is the name of the field. This returns the size of the file, in
-bytes. (Behind the scenes, it uses ``os.path.getsize``.)
+**Deprecated in Django development version**; use ``object.FOO.size`` instead.
+See `managing files`_ for details.
save_FOO_file(filename, raw_contents)
-------------------------------------
-For every ``FileField``, the object will have a ``save_FOO_file()`` method,
-where ``FOO`` is the name of the field. This saves the given file to the
-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.
+**Deprecated in Django development version**; use ``object.FOO.save()`` instead.
+See `managing files`_ for details.
get_FOO_height() and get_FOO_width()
------------------------------------
-For every ``ImageField``, the object will have ``get_FOO_height()`` and
-``get_FOO_width()`` methods, where ``FOO`` is the name of the field. This
-returns the height (or width) of the image, as an integer, in pixels.
+**Deprecated in Django development version**; use ``object.FOO.width`` and
+``object.FOO.height`` instead. See `managing files`_ for details.
+
+.. _`managing files`: ../files/
Shortcuts
=========
View
388 docs/files.txt
@@ -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.
View
58 docs/model-api.txt
@@ -224,26 +224,64 @@ set to 75 by default, but you can specify it to override default behavior.
``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
====================== ===================================================
- ``upload_to`` A local filesystem path that will be appended to
- your ``MEDIA_ROOT`` setting to determine the
- output of the ``get_<fieldname>_url()`` helper
- function.
+ ``upload_to`` Required. A filesystem-style path that will be
+ prepended to the filename before being committed to
+ the final storage destination.
+
+ **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
-date/time of the file upload (so that uploaded files don't fill up the given
-directory).
+.. _managing files: ../files/
+
+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
widget).
-Using a ``FileField`` or an ``ImageField`` (see below) in a model takes a few
-steps:
+Using a ``FileField`` or an ``ImageField`` (see below) in a model without a
+specified storage system takes a few steps:
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
View
10 docs/settings.txt
@@ -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
``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
------------------
View
25 docs/upload_handling.txt
@@ -155,25 +155,8 @@ Three `settings`_ control Django's file upload behavior:
``UploadedFile`` objects
========================
-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.
+In addition to those inherited from `File`_, all ``UploadedFile`` objects define
+the following methods/attributes:
``UploadedFile.content_type``
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
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()``
Only files uploaded onto disk will have this method; it returns the full
path to the temporary uploaded file.
+.. _File: ../files/
Upload Handlers
===============
View
1  tests/modeltests/files/__init__.py
@@ -0,0 +1 @@
+
View
118 tests/modeltests/files/models.py
@@ -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()
+"""}
View
69 tests/modeltests/model_forms/models.py
@@ -11,6 +11,9 @@
import tempfile
from django.db import models
+from django.core.files.storage import FileSystemStorage
+
+temp_storage = FileSystemStorage(tempfile.gettempdir())
ARTICLE_STATUS = (
(1, 'Draft'),
@@ -60,7 +63,7 @@ def __unicode__(self):
class TextFile(models.Model):
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):
return self.description
@@ -73,9 +76,9 @@ class ImageFile(models.Model):
# for PyPy, you need to check for the underlying modules
# If PIL is not available, this test is equivalent to TextFile above.
import Image, _imaging
- image = models.ImageField(upload_to=tempfile.gettempdir())
+ image = models.ImageField(storage=temp_storage, upload_to='tests')
except ImportError:
- image = models.FileField(upload_to=tempfile.gettempdir())
+ image = models.FileField(storage=temp_storage, upload_to='tests')
def __unicode__(self):
return self.description
@@ -786,6 +789,8 @@ def __unicode__(self):
# FileField ###################################################################
+# File forms.
+
>>> class TextFileForm(ModelForm):
... class Meta:
... model = TextFile
@@ -808,9 +813,9 @@ def __unicode__(self):
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
>>> instance = f.save()
>>> 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.is_valid()
@@ -819,7 +824,7 @@ def __unicode__(self):
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
>>> instance = f.save()
>>> instance.file
-u'...test1.txt'
+<FieldFile: tests/test1.txt>
# 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.
@@ -828,13 +833,13 @@ def __unicode__(self):
>>> f.is_valid()
True
>>> f.cleaned_data['file']
-u'...test1.txt'
+<FieldFile: tests/test1.txt>
>>> instance = f.save()
>>> instance.file
-u'...test1.txt'
+<FieldFile: tests/test1.txt>
# 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.
@@ -843,20 +848,20 @@ def __unicode__(self):
True
>>> instance = f.save()
>>> instance.file
-u'...test2.txt'
+<FieldFile: tests/test2.txt>
# 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.is_valid()
True
>>> instance = f.save()
>>> instance.file
-u'...test2.txt'
+<FieldFile: tests/test2.txt>
# Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_file_filename())
+>>> instance.file.delete()
>>> instance.delete()
@@ -868,17 +873,17 @@ def __unicode__(self):
True
>>> instance = f.save()
>>> instance.file
-''
+<FieldFile: None>
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}, instance=instance)
>>> f.is_valid()
True
>>> instance = f.save()
>>> instance.file
-u'...test3.txt'
+<FieldFile: tests/test3.txt>
# Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_file_filename())
+>>> instance.file.delete()
>>> instance.delete()
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')})
@@ -886,10 +891,10 @@ def __unicode__(self):
True
>>> instance = f.save()
>>> instance.file
-u'...test3.txt'
+<FieldFile: tests/test3.txt>
# Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_file_filename())
+>>> instance.file.delete()
>>> instance.delete()
# ImageField ###################################################################
@@ -911,10 +916,10 @@ def __unicode__(self):
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
>>> instance = f.save()
>>> instance.image
-u'...test.png'
+<ImageFieldFile: tests/test.png>
# 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.is_valid()
@@ -923,7 +928,7 @@ def __unicode__(self):
<class 'django.core.files.uploadedfile.SimpleUploadedFile'>
>>> instance = f.save()
>>> instance.image
-u'...test.png'
+<ImageFieldFile: tests/test.png>
# 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.
@@ -932,14 +937,14 @@ def __unicode__(self):
>>> f.is_valid()
True
>>> f.cleaned_data['image']
-u'...test.png'
+<ImageFieldFile: tests/test.png>
>>> instance = f.save()
>>> instance.image
-u'...test.png'
+<ImageFieldFile: tests/test.png>
# 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.
@@ -948,10 +953,10 @@ def __unicode__(self):
True
>>> instance = f.save()
>>> instance.image
-u'...test2.png'
+<ImageFieldFile: tests/test2.png>
# Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_image_filename())
+>>> instance.image.delete()
>>> instance.delete()
>>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': SimpleUploadedFile('test2.png', image_data)})
@@ -959,10 +964,10 @@ def __unicode__(self):
True
>>> instance = f.save()
>>> instance.image
-u'...test2.png'
+<ImageFieldFile: tests/test2.png>
# Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_image_filename())
+>>> instance.image.delete()
>>> instance.delete()
# Test the non-required ImageField
@@ -973,17 +978,17 @@ def __unicode__(self):
True
>>> instance = f.save()
>>> instance.image
-''
+<ImageFieldFile: None>
>>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test3.png', image_data)}, instance=instance)
>>> f.is_valid()
True
>>> instance = f.save()
>>> instance.image
-u'...test3.png'
+<ImageFieldFile: tests/test3.png>
# Delete the current file since this is not done by Django.
->>> os.unlink(instance.get_image_filename())
+>>> instance.image.delete()
>>> instance.delete()
>>> f = ImageFileForm(data={'description': u'And a final one'}, files={'image': SimpleUploadedFile('test3.png', image_data)})
@@ -991,7 +996,7 @@ def __unicode__(self):
True
>>> instance = f.save()
>>> instance.image
-u'...test3.png'
+<ImageFieldFile: tests/test3.png>
>>> instance.delete()
# Media on a ModelForm ########################################################
View
14 tests/regressiontests/admin_widgets/models.py
@@ -1,6 +1,7 @@
from django.conf import settings
from django.db import models
+from django.core.files.storage import default_storage