Added ImageField Support
Thanks to @wpjunior for the patch Closes [#298]
This commit is contained in:
parent
165cdc8840
commit
56d1139d71
@ -5,6 +5,7 @@ Changelog
|
|||||||
Changes in dev
|
Changes in dev
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
- Added ImageField - requires PIL
|
||||||
- Fixed Reference Fields can be None in get_or_create / queries
|
- Fixed Reference Fields can be None in get_or_create / queries
|
||||||
- Fixed accessing pk on an embedded document
|
- Fixed accessing pk on an embedded document
|
||||||
- Fixed calling a queryset after drop_collection now recreates the collection
|
- Fixed calling a queryset after drop_collection now recreates the collection
|
||||||
|
@ -19,6 +19,7 @@ class NotRegistered(Exception):
|
|||||||
class InvalidDocumentError(Exception):
|
class InvalidDocumentError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ValidationError(Exception):
|
class ValidationError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -1,3 +1,14 @@
|
|||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
import decimal
|
||||||
|
import gridfs
|
||||||
|
import pymongo
|
||||||
|
import pymongo.binary
|
||||||
|
import pymongo.dbref
|
||||||
|
import pymongo.son
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
from base import (BaseField, ComplexBaseField, ObjectIdField,
|
from base import (BaseField, ComplexBaseField, ObjectIdField,
|
||||||
ValidationError, get_document)
|
ValidationError, get_document)
|
||||||
from queryset import DO_NOTHING
|
from queryset import DO_NOTHING
|
||||||
@ -5,15 +16,17 @@ from document import Document, EmbeddedDocument
|
|||||||
from connection import _get_db
|
from connection import _get_db
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
import re
|
|
||||||
import pymongo
|
try:
|
||||||
import pymongo.dbref
|
from PIL import Image, ImageOps
|
||||||
import pymongo.son
|
except ImportError:
|
||||||
import pymongo.binary
|
Image = None
|
||||||
import datetime, time
|
ImageOps = None
|
||||||
import decimal
|
|
||||||
import gridfs
|
try:
|
||||||
import uuid
|
from cStringIO import StringIO
|
||||||
|
except ImportError:
|
||||||
|
from StringIO import StringIO
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField',
|
__all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField',
|
||||||
@ -21,7 +34,7 @@ __all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField',
|
|||||||
'ObjectIdField', 'ReferenceField', 'ValidationError', 'MapField',
|
'ObjectIdField', 'ReferenceField', 'ValidationError', 'MapField',
|
||||||
'DecimalField', 'ComplexDateTimeField', 'URLField',
|
'DecimalField', 'ComplexDateTimeField', 'URLField',
|
||||||
'GenericReferenceField', 'FileField', 'BinaryField',
|
'GenericReferenceField', 'FileField', 'BinaryField',
|
||||||
'SortedListField', 'EmailField', 'GeoPointField',
|
'SortedListField', 'EmailField', 'GeoPointField', 'ImageField',
|
||||||
'SequenceField', 'UUIDField', 'GenericEmbeddedDocumentField']
|
'SequenceField', 'UUIDField', 'GenericEmbeddedDocumentField']
|
||||||
|
|
||||||
RECURSIVE_REFERENCE_CONSTANT = 'self'
|
RECURSIVE_REFERENCE_CONSTANT = 'self'
|
||||||
@ -784,10 +797,12 @@ class GridFSProxy(object):
|
|||||||
|
|
||||||
.. versionadded:: 0.4
|
.. versionadded:: 0.4
|
||||||
.. versionchanged:: 0.5 - added optional size param to read
|
.. versionchanged:: 0.5 - added optional size param to read
|
||||||
|
.. versionchanged:: 0.6 - added collection name param
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, grid_id=None, key=None, instance=None):
|
def __init__(self, grid_id=None, key=None,
|
||||||
self.fs = gridfs.GridFS(_get_db()) # Filesystem instance
|
instance=None, collection_name='fs'):
|
||||||
|
self.fs = gridfs.GridFS(_get_db(), collection_name) # Filesystem instance
|
||||||
self.newfile = None # Used for partial writes
|
self.newfile = None # Used for partial writes
|
||||||
self.grid_id = grid_id # Store GridFS id for file
|
self.grid_id = grid_id # Store GridFS id for file
|
||||||
self.gridout = None
|
self.gridout = None
|
||||||
@ -878,9 +893,11 @@ class FileField(BaseField):
|
|||||||
.. versionadded:: 0.4
|
.. versionadded:: 0.4
|
||||||
.. versionchanged:: 0.5 added optional size param for read
|
.. versionchanged:: 0.5 added optional size param for read
|
||||||
"""
|
"""
|
||||||
|
proxy_class = GridFSProxy
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, collection_name="fs", **kwargs):
|
||||||
super(FileField, self).__init__(**kwargs)
|
super(FileField, self).__init__(**kwargs)
|
||||||
|
self.collection_name = collection_name
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
@ -889,12 +906,13 @@ class FileField(BaseField):
|
|||||||
# Check if a file already exists for this model
|
# Check if a file already exists for this model
|
||||||
grid_file = instance._data.get(self.name)
|
grid_file = instance._data.get(self.name)
|
||||||
self.grid_file = grid_file
|
self.grid_file = grid_file
|
||||||
if isinstance(self.grid_file, GridFSProxy):
|
if isinstance(self.grid_file, self.proxy_class):
|
||||||
if not self.grid_file.key:
|
if not self.grid_file.key:
|
||||||
self.grid_file.key = self.name
|
self.grid_file.key = self.name
|
||||||
self.grid_file.instance = instance
|
self.grid_file.instance = instance
|
||||||
return self.grid_file
|
return self.grid_file
|
||||||
return GridFSProxy(key=self.name, instance=instance)
|
return self.proxy_class(key=self.name, instance=instance,
|
||||||
|
collection_name=self.collection_name)
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
key = self.name
|
key = self.name
|
||||||
@ -911,7 +929,8 @@ class FileField(BaseField):
|
|||||||
grid_file.put(value)
|
grid_file.put(value)
|
||||||
else:
|
else:
|
||||||
# Create a new proxy object as we don't already have one
|
# Create a new proxy object as we don't already have one
|
||||||
instance._data[key] = GridFSProxy(key=key, instance=instance)
|
instance._data[key] = self.proxy_class(key=key, instance=instance,
|
||||||
|
collection_name=self.collection_name)
|
||||||
instance._data[key].put(value)
|
instance._data[key].put(value)
|
||||||
else:
|
else:
|
||||||
instance._data[key] = value
|
instance._data[key] = value
|
||||||
@ -920,20 +939,180 @@ class FileField(BaseField):
|
|||||||
|
|
||||||
def to_mongo(self, value):
|
def to_mongo(self, value):
|
||||||
# Store the GridFS file id in MongoDB
|
# Store the GridFS file id in MongoDB
|
||||||
if isinstance(value, GridFSProxy) and value.grid_id is not None:
|
if isinstance(value, self.proxy_class) and value.grid_id is not None:
|
||||||
return value.grid_id
|
return value.grid_id
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return GridFSProxy(value)
|
return self.proxy_class(value,
|
||||||
|
collection_name=self.collection_name)
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if value.grid_id is not None:
|
if value.grid_id is not None:
|
||||||
assert isinstance(value, GridFSProxy)
|
assert isinstance(value, self.proxy_class)
|
||||||
assert isinstance(value.grid_id, pymongo.objectid.ObjectId)
|
assert isinstance(value.grid_id, pymongo.objectid.ObjectId)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageGridFsProxy(GridFSProxy):
|
||||||
|
"""
|
||||||
|
Proxy for ImageField
|
||||||
|
|
||||||
|
versionadded: 0.6
|
||||||
|
"""
|
||||||
|
def put(self, file_obj, **kwargs):
|
||||||
|
"""
|
||||||
|
Insert a image in database
|
||||||
|
applying field properties (size, thumbnail_size)
|
||||||
|
"""
|
||||||
|
field = self.instance._fields[self.key]
|
||||||
|
|
||||||
|
try:
|
||||||
|
img = Image.open(file_obj)
|
||||||
|
except:
|
||||||
|
raise ValidationError('Invalid image')
|
||||||
|
|
||||||
|
if (field.size and (img.size[0] > field.size['width'] or
|
||||||
|
img.size[1] > field.size['height'])):
|
||||||
|
size = field.size
|
||||||
|
|
||||||
|
if size['force']:
|
||||||
|
img = ImageOps.fit(img,
|
||||||
|
(size['width'],
|
||||||
|
size['height']),
|
||||||
|
Image.ANTIALIAS)
|
||||||
|
else:
|
||||||
|
img.thumbnail((size['width'],
|
||||||
|
size['height']),
|
||||||
|
Image.ANTIALIAS)
|
||||||
|
|
||||||
|
thumbnail = None
|
||||||
|
if field.thumbnail_size:
|
||||||
|
size = field.thumbnail_size
|
||||||
|
|
||||||
|
if size['force']:
|
||||||
|
thumbnail = ImageOps.fit(img,
|
||||||
|
(size['width'],
|
||||||
|
size['height']),
|
||||||
|
Image.ANTIALIAS)
|
||||||
|
else:
|
||||||
|
thumbnail = img.copy()
|
||||||
|
thumbnail.thumbnail((size['width'],
|
||||||
|
size['height']),
|
||||||
|
Image.ANTIALIAS)
|
||||||
|
|
||||||
|
if thumbnail:
|
||||||
|
thumb_id = self._put_thumbnail(thumbnail,
|
||||||
|
img.format)
|
||||||
|
else:
|
||||||
|
thumb_id = None
|
||||||
|
|
||||||
|
w, h = img.size
|
||||||
|
|
||||||
|
io = StringIO()
|
||||||
|
img.save(io, img.format)
|
||||||
|
io.seek(0)
|
||||||
|
|
||||||
|
return super(ImageGridFsProxy, self).put(io,
|
||||||
|
width=w,
|
||||||
|
height=h,
|
||||||
|
format=img.format,
|
||||||
|
thumbnail_id=thumb_id,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
#deletes thumbnail
|
||||||
|
out = self.get()
|
||||||
|
if out and out.thumbnail_id:
|
||||||
|
self.fs.delete(out.thumbnail_id)
|
||||||
|
|
||||||
|
return super(ImageGridFsProxy, self).delete(*args, **kwargs)
|
||||||
|
|
||||||
|
def _put_thumbnail(self, thumbnail, format, **kwargs):
|
||||||
|
w, h = thumbnail.size
|
||||||
|
|
||||||
|
io = StringIO()
|
||||||
|
thumbnail.save(io, format)
|
||||||
|
io.seek(0)
|
||||||
|
|
||||||
|
return self.fs.put(io, width=w,
|
||||||
|
height=h,
|
||||||
|
format=format,
|
||||||
|
**kwargs)
|
||||||
|
@property
|
||||||
|
def size(self):
|
||||||
|
"""
|
||||||
|
return a width, height of image
|
||||||
|
"""
|
||||||
|
out = self.get()
|
||||||
|
if out:
|
||||||
|
return out.width, out.height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def format(self):
|
||||||
|
"""
|
||||||
|
return format of image
|
||||||
|
ex: PNG, JPEG, GIF, etc
|
||||||
|
"""
|
||||||
|
out = self.get()
|
||||||
|
if out:
|
||||||
|
return out.format
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumbnail(self):
|
||||||
|
"""
|
||||||
|
return a gridfs.grid_file.GridOut
|
||||||
|
representing a thumbnail of Image
|
||||||
|
"""
|
||||||
|
out = self.get()
|
||||||
|
if out and out.thumbnail_id:
|
||||||
|
return self.fs.get(out.thumbnail_id)
|
||||||
|
|
||||||
|
def write(self, *args, **kwargs):
|
||||||
|
raise RuntimeError("Please use \"put\" method instead")
|
||||||
|
|
||||||
|
def writelines(self, *args, **kwargs):
|
||||||
|
raise RuntimeError("Please use \"put\" method instead")
|
||||||
|
|
||||||
|
|
||||||
|
class ImproperlyConfigured(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ImageField(FileField):
|
||||||
|
"""
|
||||||
|
A Image File storage field.
|
||||||
|
|
||||||
|
@size (width, height, force):
|
||||||
|
max size to store images, if larger will be automatically resized
|
||||||
|
ex: size=(800, 600, True)
|
||||||
|
|
||||||
|
@thumbnail (width, height, force):
|
||||||
|
size to generate a thumbnail
|
||||||
|
|
||||||
|
.. versionadded:: 0.6
|
||||||
|
"""
|
||||||
|
proxy_class = ImageGridFsProxy
|
||||||
|
|
||||||
|
def __init__(self, size=None, thumbnail_size=None,
|
||||||
|
collection_name='images', **kwargs):
|
||||||
|
if not Image:
|
||||||
|
raise ImproperlyConfigured("PIL library was not found")
|
||||||
|
|
||||||
|
params_size = ('width', 'height', 'force')
|
||||||
|
extra_args = dict(size=size, thumbnail_size=thumbnail_size)
|
||||||
|
for att_name, att in extra_args.items():
|
||||||
|
if att and (isinstance(att, tuple) or isinstance(att, list)):
|
||||||
|
setattr(self, att_name, dict(
|
||||||
|
map(None, params_size, att)))
|
||||||
|
else:
|
||||||
|
setattr(self, att_name, None)
|
||||||
|
|
||||||
|
super(ImageField, self).__init__(
|
||||||
|
collection_name=collection_name,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class GeoPointField(BaseField):
|
class GeoPointField(BaseField):
|
||||||
"""A list storing a latitude and longitude.
|
"""A list storing a latitude and longitude.
|
||||||
|
|
||||||
|
2
setup.py
2
setup.py
@ -47,5 +47,5 @@ setup(name='mongoengine',
|
|||||||
classifiers=CLASSIFIERS,
|
classifiers=CLASSIFIERS,
|
||||||
install_requires=['pymongo'],
|
install_requires=['pymongo'],
|
||||||
test_suite='tests',
|
test_suite='tests',
|
||||||
tests_require=['blinker', 'django>=1.3']
|
tests_require=['blinker', 'django>=1.3', 'PIL']
|
||||||
)
|
)
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import unittest
|
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal
|
import os
|
||||||
|
import unittest
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
from mongoengine.connection import _get_db
|
from mongoengine.connection import _get_db
|
||||||
from mongoengine.base import _document_registry, NotRegistered
|
from mongoengine.base import _document_registry, NotRegistered
|
||||||
|
|
||||||
|
TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), 'mongoengine.png')
|
||||||
|
|
||||||
|
|
||||||
class FieldTest(unittest.TestCase):
|
class FieldTest(unittest.TestCase):
|
||||||
|
|
||||||
@ -1392,6 +1396,67 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
TestFile.drop_collection()
|
TestFile.drop_collection()
|
||||||
|
|
||||||
|
def test_image_field(self):
|
||||||
|
|
||||||
|
class TestImage(Document):
|
||||||
|
image = ImageField()
|
||||||
|
|
||||||
|
TestImage.drop_collection()
|
||||||
|
|
||||||
|
t = TestImage()
|
||||||
|
t.image.put(open(TEST_IMAGE_PATH, 'r'))
|
||||||
|
t.save()
|
||||||
|
|
||||||
|
t = TestImage.objects.first()
|
||||||
|
|
||||||
|
self.assertEquals(t.image.format, 'PNG')
|
||||||
|
|
||||||
|
w, h = t.image.size
|
||||||
|
self.assertEquals(w, 371)
|
||||||
|
self.assertEquals(h, 76)
|
||||||
|
|
||||||
|
t.image.delete()
|
||||||
|
|
||||||
|
def test_image_field_resize(self):
|
||||||
|
|
||||||
|
class TestImage(Document):
|
||||||
|
image = ImageField(size=(185, 37))
|
||||||
|
|
||||||
|
TestImage.drop_collection()
|
||||||
|
|
||||||
|
t = TestImage()
|
||||||
|
t.image.put(open(TEST_IMAGE_PATH, 'r'))
|
||||||
|
t.save()
|
||||||
|
|
||||||
|
t = TestImage.objects.first()
|
||||||
|
|
||||||
|
self.assertEquals(t.image.format, 'PNG')
|
||||||
|
w, h = t.image.size
|
||||||
|
|
||||||
|
self.assertEquals(w, 185)
|
||||||
|
self.assertEquals(h, 37)
|
||||||
|
|
||||||
|
t.image.delete()
|
||||||
|
|
||||||
|
def test_image_field_thumbnail(self):
|
||||||
|
|
||||||
|
class TestImage(Document):
|
||||||
|
image = ImageField(thumbnail_size=(92, 18))
|
||||||
|
|
||||||
|
TestImage.drop_collection()
|
||||||
|
|
||||||
|
t = TestImage()
|
||||||
|
t.image.put(open(TEST_IMAGE_PATH, 'r'))
|
||||||
|
t.save()
|
||||||
|
|
||||||
|
t = TestImage.objects.first()
|
||||||
|
|
||||||
|
self.assertEquals(t.image.thumbnail.format, 'PNG')
|
||||||
|
self.assertEquals(t.image.thumbnail.width, 92)
|
||||||
|
self.assertEquals(t.image.thumbnail.height, 18)
|
||||||
|
|
||||||
|
t.image.delete()
|
||||||
|
|
||||||
def test_geo_indexes(self):
|
def test_geo_indexes(self):
|
||||||
"""Ensure that indexes are created automatically for GeoPointFields.
|
"""Ensure that indexes are created automatically for GeoPointFields.
|
||||||
"""
|
"""
|
||||||
|
BIN
tests/mongoengine.png
Normal file
BIN
tests/mongoengine.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
Loading…
x
Reference in New Issue
Block a user