Index: django/conf/global_settings.py
===================================================================
--- django/conf/global_settings.py (revision 8222)
+++ django/conf/global_settings.py (working copy)
@@ -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 = ''
Index: django/contrib/admin/widgets.py
===================================================================
--- django/contrib/admin/widgets.py (revision 8222)
+++ django/contrib/admin/widgets.py (working copy)
@@ -85,8 +85,8 @@
def render(self, name, value, attrs=None):
output = []
if value:
- output.append('%s %s %s ' % \
- (_('Currently:'), settings.MEDIA_URL, value, value, _('Change:')))
+ output.append('%s %s %s ' % \
+ (_('Currently:'), value.url, value, _('Change:')))
output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(u''.join(output))
Index: django/core/files/__init__.py
===================================================================
--- django/core/files/__init__.py (revision 8222)
+++ django/core/files/__init__.py (working copy)
@@ -0,0 +1 @@
+from django.core.files.base import File
Index: django/core/files/base.py
===================================================================
--- django/core/files/base.py (revision 0)
+++ django/core/files/base.py (revision 0)
@@ -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)
Index: django/core/files/images.py
===================================================================
--- django/core/files/images.py (revision 0)
+++ django/core/files/images.py (revision 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
Index: django/core/files/storage.py
===================================================================
--- django/core/files/storage.py (revision 0)
+++ django/core/files/storage.py (revision 0)
@@ -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()
Index: django/core/files/uploadedfile.py
===================================================================
--- django/core/files/uploadedfile.py (revision 8222)
+++ django/core/files/uploadedfile.py (working copy)
@@ -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 @@
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 @@
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 @@
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 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(
Index: django/db/models/__init__.py
===================================================================
--- django/db/models/__init__.py (revision 8222)
+++ django/db/models/__init__.py (working copy)
@@ -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
Index: django/db/models/base.py
===================================================================
--- django/db/models/base.py (revision 8222)
+++ django/db/models/base.py (working copy)
@@ -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
@@ -465,112 +466,44 @@
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))
+ 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):
- # 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)
+ 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)
- # 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
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]
+ 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()
- 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)
-
############################################
# HELPER FUNCTIONS (CURRIED MODEL METHODS) #
############################################
Index: django/db/models/fields/__init__.py
===================================================================
--- django/db/models/fields/__init__.py (revision 8222)
+++ django/db/models/fields/__init__.py (working copy)
@@ -8,7 +8,6 @@
from django.utils import _decimal as decimal # for Python 2.3
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
@@ -758,131 +757,6 @@
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))
- dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls)
-
- def delete_file(self, instance):
- 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
@@ -924,40 +798,6 @@
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):
Index: django/db/models/fields/files.py
===================================================================
--- django/db/models/fields/files.py (revision 0)
+++ django/db/models/fields/files.py (revision 0)
@@ -0,0 +1,332 @@
+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
+from django.core.files.uploadedfile import UploadedFile
+from django.utils.functional import curry
+from django.dispatch import dispatcher
+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))
+ dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls)
+
+ def delete_file(self, instance, sender):
+ 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):
+ super(ImageFieldFile, self).save(name, content, save)
+
+ # Update the cache for image dimensions
+ from django.core.files.images import get_image_dimensions
+ if not hasattr(content, 'read'):
+ content = ContentFile(name, content)
+ self._dimensions_cache = get_image_dimensions(content)
+
+ 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 save_form_data(self, instance, data):
+ # 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 self.width_field or self.height_field:
+ if original_object and not change:
+ 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))
+ else:
+ from django.utils.images import get_image_dimensions
+
+ upload_field_name = self.get_manipulator_field_names('')[0]
+ if rel:
+ file = new_data[upload_field_name][0]
+ else:
+ file = new_data[upload_field_name]
+
+ # Get the width and height from the raw content to avoid extra
+ # unnecessary trips to the file backend.
+ width, height = get_image_dimensions(file)
+
+ if self.width_field:
+ setattr(new_object, self.width_field, width)
+ if self.height_field:
+ setattr(new_object, self.height_field, height)
+ super(ImageField, self).save_form_data(instance, data)
+
+ def formfield(self, **kwargs):
+ defaults = {'form_class': forms.ImageField}
+ defaults.update(kwargs)
+ return super(ImageField, self).formfield(**defaults)
Index: django/db/models/manipulators.py
===================================================================
--- django/db/models/manipulators.py (revision 8222)
+++ django/db/models/manipulators.py (working copy)
@@ -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.dispatch import dispatcher
from django.db.models import signals
from django.utils.functional import curry
Index: django/utils/images.py
===================================================================
--- django/utils/images.py (revision 8222)
+++ django/utils/images.py (working copy)
@@ -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)
Index: docs/custom_model_fields.txt
===================================================================
--- docs/custom_model_fields.txt (revision 8222)
+++ docs/custom_model_fields.txt (working copy)
@@ -596,3 +596,42 @@
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.
Index: docs/db-api.txt
===================================================================
--- docs/db-api.txt (revision 8222)
+++ docs/db-api.txt (working copy)
@@ -2298,6 +2298,9 @@
get_FOO_filename()
------------------
+**Deprecated in Django development version. See `managing files`_ for the new,
+preferred method for dealing with files.**
+
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.
@@ -2313,6 +2316,9 @@
get_FOO_url()
-------------
+**Deprecated in Django development version. See `managing files`_ for the new,
+preferred method for dealing with files.**
+
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
@@ -2326,6 +2332,9 @@
get_FOO_size()
--------------
+**Deprecated in Django development version. See `managing files`_ for the new,
+preferred method for dealing with files.**
+
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``.)
@@ -2333,6 +2342,9 @@
save_FOO_file(filename, raw_contents)
-------------------------------------
+**Deprecated in Django development version. See `managing files`_ for the new,
+preferred method for dealing with files.**
+
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
@@ -2342,10 +2354,15 @@
get_FOO_height() and get_FOO_width()
------------------------------------
+**Deprecated in Django development version. See `managing files`_ for the new,
+preferred method for dealing with files.**
+
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.
+.. _`managing files`: ../files/
+
Shortcuts
=========
Index: docs/files.txt
===================================================================
--- docs/files.txt (revision 0)
+++ docs/files.txt (revision 0)
@@ -0,0 +1,390 @@
+==============
+Managing files
+==============
+
+**New in Django development version**
+
+When dealing with files, Django provides a number of features to make this task
+easier and more portable. A storage protocol is available to allow files to be
+stored in a variety of locations, and a special object is provided to allow
+models to make use of this protocol, without having to worry about which storage
+system is being used.
+
+The ``File`` object
+===================
+
+Any time Django references a file, it will be an instance of the ``File`` class,
+living at ``django.core.files``, or one of its subclasses. This provides many of
+the same features as Python's own `file object`_, but has a few additions and
+is customized for working with large-scale applications.
+
+.. _file object: http://docs.python.org/lib/bltin-file-objects.html
+
+Example
+-------
+
+While there are several `File` subclasses that get created automatically, for
+things like file uploading, the simplest way to get access to a ``File`` is to
+just instantiate one using a true ``file`` from Python's own ``open()``::
+
+ >>> from django.core.files import File
+
+ >>> writable = File(open('/tmp/hello_world', 'w'))
+ >>> writable.name, writable.mode
+ ('/tmp/hello_world', 'w')
+ >>> writable.write('Hello, world!')
+ >>> writable.close()
+
+ >>> readable = File(open('/tmp/hello_world'))
+ >>> readable.size
+ 13
+ >>> readable.close()
+
+``path``
+--------
+
+Returns the absolute path to the file's location on a local filesystem. For
+storage systems which do not store files locally, this will return ``None``.
+
+``url``
+-------
+
+Provides a URL where the content of the file can be retrieved. Therefore,
+returned from this method is suitable for use as the destination of a link to
+the file.
+
+``size``
+--------
+
+Returns the size of the file, as an integer.
+
+``open(mode=None)``
+-------------------
+
+Reopens the file, resetting the internal pointer back to the beginning. The
+``mode`` argument allows the same values as Python's standard ``open()``. If
+provided, ``mode`` will override whatever mode the file was opened with, while
+omitting it will reopen the file using the same mode used before it was closed.
+
+``read(num_bytes=None)``
+------------------------
+
+Retrieves some content from the file, returning it as a string. The optional
+``size`` is the number of bytes to read; if not specified, the file will be read
+through to the end.
+
+``__iter__()``
+--------------
+
+Iterates over the lines in the file, preserving newline characters on each line.
+This allows the file to be used in simple loops, using ``for line in file``.
+
+``chunks(chunk_size=None)``
+---------------------------
+
+Yields the contents of the file in smaller chunks, that can be looped over to
+access the file without loading it all into memory at once. If ``chunk_size``
+isn't provided, it defaults to 64 KB.
+
+``multiple_chunks(chunk_size=None)``
+------------------------------------
+
+Returns ``True`` if the file is large enough to require multiple chunks to
+access all of its content, or ``False`` if it can all be read in one pass. The
+optional ``chunk_size`` works the same as in ``chunks()`` above.
+
+``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.
+
+``close()``
+-----------
+
+Closes the file, so it can't be read from or written to anymore. If there's
+still any content that hasn't been written to the file itself, this will commit
+that as well.
+
+``ImageFile``
+=============
+
+Anywhere Django can open a new file, it also accepts a mixin class to support
+more specific file types. For images, there's a more specific ``ImageFile``,
+available from ``django.core.files.images``.
+
+``width and height``
+--------------------
+
+When using an ``ImageField``, these two attributes will be available, providing
+easy access to the dimensions of the image.
+
+Using files in models
+=====================
+
+When accessing a ``FileField`` attached to a model, a special object provides
+access to the file and information about it.
+
+Example
+-------
+
+Consider the following model, using an ``ImageField`` to store a product photo::
+
+ class Product(models.Model):
+ name = models.CharField(maxlength=255)
+ price = models.DecimalField(max_digits=5, decimal_places=2)
+ photo = models.ImageField(upload_to='product_photos')
+
+Your views can then use the ``photo`` attribute with the functions described
+above, as follows::
+
+ >>> car = Product.object.get(name="'57 Chevy")
+ >>> car.photo
+
+ >>> car.photo.url
+ '/products/photo/123.jpg'
+ >>> car.photo.width, car.photo.height
+ (800, 600)
+
+``save(name, content, save=True)``
+----------------------------------
+
+Saves a new file with the filename and contents provided. This will not replace
+the existing file, but will create a new file and update the object to point to
+it. The optional ``save`` argument dictates whether the model instance will be
+saved to the database immediately.
+
+``delete(save=True)``
+---------------------
+
+Removes the file from the model instance and deletes it from the underlying
+storage system. The optional ``save`` argument indicates whether the model
+instance will saved to the database immediately.
+
+Using a storage system with FileField
+=====================================
+
+When using a storage system, supply whatever options are appropriate for
+that system when creating a new object. Then pass that object as the ``storage``
+argument to a ``FileField``. Details on the requirements for the included
+storage system can be found below.
+
+If using the default storage system, it is not necessary to create a storage
+object explicitly. In this case, the ``FileField`` will use the one referenced
+by the `DEFAULT_FILE_STORAGE setting`_.
+
+See the `FileField documentation`_ for more information on using the field.
+
+.. _DEFAULT_FILE_STORAGE setting: ../settings/#default-file-storage
+.. _FileField documentation: ../model-api/#filefield
+
+For example, the following code will explicitly use the ``FileSystemStorage``::
+
+ from django.db import models
+ from django.core.files.storage import FileSystemStorage
+
+ fs = FileSystemStorage(location='product_photos')
+
+ class Product(models.Model):
+ name = models.CharField(maxlength=255)
+ price = models.DecimalField(max_digits=5, decimal_places=2)
+ photo = models.ImageField(storage=fs)
+
+Using a storage system on its own
+=================================
+
+Storage systems may also be used directly, without being attached to a model.
+Simply use the following API on any instantiated storage system to access files
+without having to worry about the underlying mechanism. In addition to explicit
+storage mechanisms, the file storage module, ``django.core.files.storage``,
+exports a ``default_storage`` object that's automatically created from the
+``DEFAULT_FILE_STORAGE`` setting::
+
+ >>> from django.core.files.storage import default_storage
+
+With a functional storage system on hand, managing files is quite simple, with a
+few basic methods to handle the most common operations::
+
+ >>> path = storage.save('/path/to/file', 'new content')
+ >>> path
+ u'/path/to/file'
+ >>> storage.filesize(path)
+ 11
+ >>> storage.open(path).read()
+ 'new content'
+ >>> storage.delete(path)
+ >>> storage.exists(path)
+ False
+
+``exists(name)``
+----------------
+
+Returns ``True`` or ``False, indicating whether there is already a file present
+at the location referenced by``name``.
+
+``path(name)``
+--------------
+
+Returns 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.
+
+``size(name)``
+--------------
+
+Returns the total size, in bytes, of the file referenced by ``name``.
+
+``url(name)``
+-------------
+
+Returns the URL where the contents of the file referenced by ``name`` can be
+accessed.
+
+``open(name, mode='rb', mixin=None)``
+-------------------------------------
+
+Returns an open file, or file-like, object to provide access to the contents of
+the file referenced by ``name``. The ``mode`` argument allows the same values as
+Python's standard ``open()`` function. The ``mixin`` is an optional class that,
+if provided, will be applied to the ``File`` object returned from this method.
+
+``save(name, content)``
+-----------------------
+
+Saves a new file using the storage system, preferably with the name specified.
+If there already exists a file at the location referenced by ``name``, this may
+modify the filename as necessary to locate one that is available. Once the file
+is saved, this method will return the filename where the file was actually
+stored.
+
+``delete(name)``
+----------------
+
+Deletes the file referenced by ``name``. If the file does not already exist,
+this method will simply return without raising an exception.
+
+Available storage systems
+=========================
+
+Only one storage system is supplied in the official Django distribution, but
+more may be available elsewhere. If you'd like to use a different storage system
+than the one listed below, see the documentation included with it.
+
+``django.core.files.storage.FileSystemStorage``
+-----------------------------------------------
+
+This simply stores files on the system's standard filesystem.
+
+ ====================== ===================================================
+ 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.
+ ====================== ===================================================
+
+Writing a storage system
+========================
+
+While the default filesystem storage is suitable for most needs, there are many
+other storage mechanisms that may be used, and situations that will require
+special processing. In order to use Django in these environments, it's fairly
+simple to write a new storage system, creating a wrapper around whatever
+libraries are used to access your files, or simply customizing method calls on
+an existing storage class.
+
+If a storage system requires any configuration options to determine how it
+should access the underlying storage mechanism or cusotmize its behavior in
+other ways, those options should be specified in a particular way. Because the
+default storage system is specified as a string, Django must be able to
+instantiate it without any arguments, and any required arguments should be
+specified as global settings, which can be referenced from the storage system.
+For example::
+
+ from django.conf import settings
+ from django.core.files.storage import Storage
+
+ class CustomStorage(Storage):
+ def __init__(self, option=settings.CUSTOM_STORAGE_OPTION):
+ ...
+
+Every storage system will have all the methods described above, but there are a
+few with have default behaviors that shouldn't be overridden by these subclasses
+in most situations. Each of these has a different set of responsibilities that
+the storage system is expected to fulfill:
+
+ * ``path()`` -- unless the class provides access to files that are also
+ accessible via the local filesystem, this should inherit the default behavior
+ of raising a ``NotImplementedError``. For those that do represent portions of
+ the filesystem, subclassing ``FileSystemStorage`` will typically be more
+ appropriate anyway.
+
+ * ``open()`` -- This provides some additional logic that isn't specific to
+ file retrieval, by supporting the ``mixin`` argument. Instead of overriding
+ this directly, storage systems should provide an ``_open()`` method as
+ described below.
+
+ * ``save()`` -- The ``name`` provided to this is actually more a preference,
+ because it will actually go through both ``get_valid_name()`` and
+ ``get_available_name()`` to determine what name the will actually be given.
+ It also returns the final name, taking care to adjust it to Unix-style paths.
+ Since these features aren't related to actually storing the file, subclasses
+ should instead provide a ``_save()`` method as described below.
+
+The default beaviors for these methods are provided by the provided ``Storage``
+class, living at ``django.files.storage``. In addition, the two other methods
+used by ``save()`` internally to determine the final filename, which have
+default implementations, but can be overridden, and there are two other methods
+that must be provided for all storage systems.
+
+``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.
+
+``_open(name, mode='rb')``
+--------------------------
+
+Returns an open ``File`` object that can be used to access the file's contents.
+The ``mode`` represents all the same values as Python's own ``open()``, and
+should be used to determine how the file can be accessed. See below for details
+regarding how the returned ``File`` object should be behave for reading and
+writing content.
+
+``_save(name, content)``
+------------------------
+
+Stores the given content to the persistent storage backed by the class. The
+``name`` will already have gone through ``get_valid_name()`` and
+``get_available_name()``, and the ``content`` will be a ``File`` object itself.
+This method has no return value.
+
+Providing a ``File``
+--------------------
+
+Since the ``open()`` method returns a ``File`` object, it's expected that a
+``Storage`` subclass will provide a customized version that's designed to
+interact with the underlying storage system. Many methods, such as ``read()``,
+``write()`` and ``close()``, should be overridden on this new ``File`` subclass,
+so that it can transparently access the file's contents.
Index: docs/model-api.txt
===================================================================
--- docs/model-api.txt (revision 8222)
+++ docs/model-api.txt (working copy)
@@ -224,26 +224,64 @@
``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__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 ```` (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
Index: docs/settings.txt
===================================================================
--- docs/settings.txt (revision 8222)
+++ docs/settings.txt (working copy)
@@ -426,6 +426,16 @@
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
------------------
Index: docs/upload_handling.txt
===================================================================
--- docs/upload_handling.txt (revision 8222)
+++ docs/upload_handling.txt (working copy)
@@ -155,26 +155,9 @@
``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``
The content-type header uploaded with the file (e.g. ``text/plain`` or
``application/pdf``). Like any data supplied by the user, you shouldn't
@@ -186,13 +169,11 @@
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
===============
Property changes on: tests\modeltests\files
___________________________________________________________________
Name: svn:ignore
+ *.pyc
Index: tests/modeltests/files/__init__.py
===================================================================
--- tests/modeltests/files/__init__.py (revision 0)
+++ tests/modeltests/files/__init__.py (revision 0)
@@ -0,0 +1 @@
+
Index: tests/modeltests/files/models.py
===================================================================
--- tests/modeltests/files/models.py (revision 0)
+++ tests/modeltests/files/models.py (revision 0)
@@ -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
+
+>>> 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
+
+>>> 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
+
+>>> 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
+
+
+# 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
+
+
+# Default values allow an object to access a single file.
+
+>>> obj3 = Storage.objects.create()
+>>> obj3.default
+
+>>> 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
+
+
+# Clean up the temporary files.
+
+>>> obj1.normal.delete()
+>>> obj2.normal.delete()
+>>> obj3.default.delete()
+>>> obj4.random.delete()
+"""}
Index: tests/modeltests/model_forms/models.py
===================================================================
--- tests/modeltests/model_forms/models.py (revision 8222)
+++ tests/modeltests/model_forms/models.py (working copy)
@@ -11,7 +11,10 @@
import tempfile
from django.db import models
+from django.core.files.storage import FileSystemStorage
+temp_storage = FileSystemStorage(tempfile.gettempdir())
+
ARTICLE_STATUS = (
(1, 'Draft'),
(2, 'Pending'),
@@ -60,7 +63,7 @@
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 @@
# 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 @@
# FileField ###################################################################
+# File forms.
+
>>> class TextFileForm(ModelForm):
... class Meta:
... model = TextFile
@@ -808,9 +813,9 @@
>>> instance = f.save()
>>> instance.file
-u'...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 @@
>>> instance = f.save()
>>> instance.file
-u'...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 @@
>>> f.is_valid()
True
>>> f.cleaned_data['file']
-u'...test1.txt'
+
>>> instance = f.save()
>>> instance.file
-u'...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 @@
True
>>> instance = f.save()
>>> instance.file
-u'...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'
+
# 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 @@
True
>>> instance = f.save()
>>> instance.file
-''
+
>>> 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'
+
# 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 @@
True
>>> instance = f.save()
>>> instance.file
-u'...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 @@
>>> instance = f.save()
>>> instance.image
-u'...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 @@
>>> instance = f.save()
>>> instance.image
-u'...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 @@
>>> f.is_valid()
True
>>> f.cleaned_data['image']
-u'...test.png'
+
>>> instance = f.save()
>>> instance.image
-u'...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 @@
True
>>> instance = f.save()
>>> instance.image
-u'...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 @@
True
>>> instance = f.save()
>>> instance.image
-u'...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 @@
True
>>> instance = f.save()
>>> instance.image
-''
+
>>> 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'
+
# 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 @@
True
>>> instance = f.save()
>>> instance.image
-u'...test3.png'
+
>>> instance.delete()
# Media on a ModelForm ########################################################
Index: tests/regressiontests/admin_widgets/models.py
===================================================================
--- tests/regressiontests/admin_widgets/models.py (revision 8222)
+++ tests/regressiontests/admin_widgets/models.py (working copy)
@@ -1,6 +1,7 @@
from django.conf import settings
from django.db import models
+from django.core.files.storage import default_storage
class Member(models.Model):
name = models.CharField(max_length=100)
@@ -18,6 +19,7 @@
class Album(models.Model):
band = models.ForeignKey(Band)
name = models.CharField(max_length=100)
+ cover_art = models.ImageField(upload_to='albums')
def __unicode__(self):
return self.name
@@ -46,13 +48,13 @@
>>> print conditional_escape(w.render('test', datetime(2007, 12, 1, 9, 30)))
Date: Time:
+>>> band = Band.objects.create(pk=1, name='Linkin Park')
+>>> album = band.album_set.create(name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg')
+
>>> w = AdminFileWidget()
->>> print conditional_escape(w.render('test', 'test'))
-Currently: test Change:
+>>> print conditional_escape(w.render('test', album.cover_art))
+Currently: albums\hybrid_theory.jpg Change:
->>> band = Band.objects.create(pk=1, name='Linkin Park')
->>> album = band.album_set.create(name='Hybrid Theory')
-
>>> rel = Album._meta.get_field('band').rel
>>> w = ForeignKeyRawIdWidget(rel)
>>> print conditional_escape(w.render('test', band.pk, attrs={}))
@@ -81,5 +83,5 @@
""" % {
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
- 'MEDIA_URL': settings.MEDIA_URL,
+ 'STORAGE_URL': default_storage.url(''),
}}
Index: tests/regressiontests/bug639/models.py
===================================================================
--- tests/regressiontests/bug639/models.py (revision 8222)
+++ tests/regressiontests/bug639/models.py (working copy)
@@ -1,16 +1,20 @@
import tempfile
+
from django.db import models
+from django.core.files.storage import FileSystemStorage
+temp_storage = FileSystemStorage(tempfile.gettempdir())
+
class Photo(models.Model):
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
# called on each instance.
def __init__(self, *args, **kwargs):
- super(Photo, self).__init__(*args, **kwargs)
- self._savecount = 0
+ super(Photo, self).__init__(*args, **kwargs)
+ self._savecount = 0
def save(self):
super(Photo, self).save()
- self._savecount +=1
\ No newline at end of file
+ self._savecount += 1
Index: tests/regressiontests/bug639/tests.py
===================================================================
--- tests/regressiontests/bug639/tests.py (revision 8222)
+++ tests/regressiontests/bug639/tests.py (working copy)
@@ -36,4 +36,4 @@
Make sure to delete the "uploaded" file to avoid clogging /tmp.
"""
p = Photo.objects.get()
- os.unlink(p.get_image_filename())
+ p.image.delete(save=False)
Index: tests/regressiontests/file_uploads/models.py
===================================================================
--- tests/regressiontests/file_uploads/models.py (revision 8222)
+++ tests/regressiontests/file_uploads/models.py (working copy)
@@ -1,9 +1,10 @@
import tempfile
import os
from django.db import models
+from django.core.files.storage import FileSystemStorage
-UPLOAD_ROOT = tempfile.mkdtemp()
-UPLOAD_TO = os.path.join(UPLOAD_ROOT, 'test_upload')
+temp_storage = FileSystemStorage(tempfile.mkdtemp())
+UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload')
class FileModel(models.Model):
- testfile = models.FileField(upload_to=UPLOAD_TO)
+ testfile = models.FileField(storage=temp_storage, upload_to='test_upload')
Index: tests/regressiontests/file_uploads/tests.py
===================================================================
--- tests/regressiontests/file_uploads/tests.py (revision 8222)
+++ tests/regressiontests/file_uploads/tests.py (working copy)
@@ -9,7 +9,7 @@
from django.utils import simplejson
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):
def test_simple_upload(self):
@@ -194,22 +194,22 @@
"""
def setUp(self):
self.obj = FileModel()
- if not os.path.isdir(UPLOAD_ROOT):
- os.makedirs(UPLOAD_ROOT)
+ if not os.path.isdir(temp_storage.location):
+ os.makedirs(temp_storage.location)
def tearDown(self):
- os.chmod(UPLOAD_ROOT, 0700)
- shutil.rmtree(UPLOAD_ROOT)
+ os.chmod(temp_storage.location, 0700)
+ shutil.rmtree(temp_storage.location)
def test_readonly_root(self):
"""Permission errors are not swallowed"""
- os.chmod(UPLOAD_ROOT, 0500)
+ os.chmod(temp_storage.location, 0500)
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:
self.assertEquals(err.errno, errno.EACCES)
- except:
- self.fail("OSError [Errno %s] not raised" % errno.EACCES)
+ except Exception, err:
+ self.fail("OSError [Errno %s] not raised." % errno.EACCES)
def test_not_a_directory(self):
"""The correct IOError is raised when the upload directory name exists but isn't a directory"""
@@ -217,11 +217,11 @@
fd = open(UPLOAD_TO, 'w')
fd.close()
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:
# The test needs to be done on a specific string as IOError
# is raised even without the patch (just not early enough)
self.assertEquals(err.args[0],
- "%s exists and is not a directory" % UPLOAD_TO)
+ "%s exists and is not a directory." % UPLOAD_TO)
except:
self.fail("IOError not raised")
Index: tests/regressiontests/serializers_regress/models.py
===================================================================
--- tests/regressiontests/serializers_regress/models.py (revision 8222)
+++ tests/regressiontests/serializers_regress/models.py (working copy)
@@ -157,8 +157,8 @@
class EmailPKData(models.Model):
data = models.EmailField(primary_key=True)
-class FilePKData(models.Model):
- data = models.FileField(primary_key=True, upload_to='/foo/bar')
+# class FilePKData(models.Model):
+# data = models.FileField(primary_key=True, upload_to='/foo/bar')
class FilePathPKData(models.Model):
data = models.FilePathField(primary_key=True)
Index: tests/regressiontests/serializers_regress/tests.py
===================================================================
--- tests/regressiontests/serializers_regress/tests.py (revision 8222)
+++ tests/regressiontests/serializers_regress/tests.py (working copy)
@@ -144,7 +144,7 @@
(data_obj, 41, EmailData, None),
(data_obj, 42, EmailData, ""),
(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, 60, FilePathData, "/foo/bar/whiz.txt"),
(data_obj, 61, FilePathData, None),
@@ -242,7 +242,7 @@
# (pk_obj, 620, DatePKData, datetime.date(2006,6,16)),
# (pk_obj, 630, DateTimePKData, datetime.datetime(2006,6,16,10,42,37)),
(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, 670, DecimalPKData, decimal.Decimal('12.345')),
(pk_obj, 671, DecimalPKData, decimal.Decimal('-12.345')),
Property changes on: tests\regressiontests\storage
___________________________________________________________________
Name: svn:ignore
+ *.pyc
Index: tests/regressiontests/storage/__init__.py
===================================================================
--- tests/regressiontests/storage/__init__.py (revision 0)
+++ tests/regressiontests/storage/__init__.py (revision 0)
@@ -0,0 +1 @@
+
Index: tests/regressiontests/storage/models.py
===================================================================
--- tests/regressiontests/storage/models.py (revision 0)
+++ tests/regressiontests/storage/models.py (revision 0)
@@ -0,0 +1 @@
+# Empty file to force tests to run
Index: tests/regressiontests/storage/tests.py
===================================================================
--- tests/regressiontests/storage/tests.py (revision 0)
+++ tests/regressiontests/storage/tests.py (revision 0)
@@ -0,0 +1,89 @@
+"""
+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.
+
+# RemoteFile allows files to be committed by way of a user-defined function.
+
+>>> from django.core.files.remote import RemoteFile
+>>> def write_file(contents):
+... print 'Writing %s' % contents
+
+# Opening for read access doesn't commit back to the server
+
+>>> file = RemoteFile('', 'r', write_file)
+>>> file.close()
+
+# The same goes for opening for write access, but not actually writing
+
+>>> file = RemoteFile('', 'w', write_file)
+>>> file.close()
+
+# But once it's written to, it gets committed on close
+
+>>> file = RemoteFile('', 'w', write_file)
+>>> file.write('remote contents') # Content isn't committed yet
+>>> file.close() # Content gets committed to the storage system
+Writing remote contents
+
+# 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)
+"""