Commit d2775fe3 authored by Serge S. Koval's avatar Serge S. Koval

Image upload field

parent 959e866b
from flask import request from werkzeug.datastructures import FileStorage
from wtforms import fields from wtforms import fields
from wtforms.fields.core import _unset_value from wtforms.fields.core import _unset_value
...@@ -29,13 +30,13 @@ class MongoFileField(fields.FileField): ...@@ -29,13 +30,13 @@ class MongoFileField(fields.FileField):
def __init__(self, label=None, validators=None, **kwargs): def __init__(self, label=None, validators=None, **kwargs):
super(MongoFileField, self).__init__(label, validators, **kwargs) super(MongoFileField, self).__init__(label, validators, **kwargs)
self.should_delete = False self._should_delete = False
def process(self, formdata, data=_unset_value): def process(self, formdata, data=_unset_value):
if formdata: if formdata:
marker = '_%s-delete' % self.name marker = '_%s-delete' % self.name
if marker in formdata: if marker in formdata:
self.should_delete = True self._should_delete = True
return super(MongoFileField, self).process(formdata, data) return super(MongoFileField, self).process(formdata, data)
...@@ -43,21 +44,19 @@ class MongoFileField(fields.FileField): ...@@ -43,21 +44,19 @@ class MongoFileField(fields.FileField):
field = getattr(obj, name, None) field = getattr(obj, name, None)
if field is not None: if field is not None:
# If field should be deleted, clean it up # If field should be deleted, clean it up
if self.should_delete: if self._should_delete:
field.delete() field.delete()
return return
data = request.files.get(self.name) if isinstance(self.data, FileStorage):
if data:
if not field.grid_id: if not field.grid_id:
field.put(data.stream, func = field.put
filename=data.filename,
content_type=data.content_type)
else: else:
field.replace(data.stream, func = field.replace
filename=data.filename,
content_type=data.content_type) func(self.data.stream,
filename=self.data.filename,
content_type=self.data.content_type)
class MongoImageField(MongoFileField): class MongoImageField(MongoFileField):
......
import os import os
import os.path as op import os.path as op
import logging
from flask import url_for
from werkzeug import secure_filename from werkzeug import secure_filename
from werkzeug.datastructures import FileStorage from werkzeug.datastructures import FileStorage
...@@ -12,7 +15,19 @@ from wtforms.fields.core import _unset_value ...@@ -12,7 +15,19 @@ from wtforms.fields.core import _unset_value
from flask.ext.admin.babel import gettext from flask.ext.admin.babel import gettext
__all__ = ['FileUploadInput', 'FileUploadField', 'namefn_keep_filename'] from flask.ext.admin._compat import string_types
try:
from PIL import Image, ImageOps
except ImportError:
Image = None
ImageOps = None
__all__ = ['FileUploadInput', 'FileUploadField',
'ImageUploadInput', 'ImageUploadField',
'namegen_filename', 'thumbgen_filename']
# Widgets # Widgets
...@@ -20,19 +35,62 @@ class FileUploadInput(object): ...@@ -20,19 +35,62 @@ class FileUploadInput(object):
""" """
Renders a file input chooser field. Renders a file input chooser field.
""" """
template = ('<input %(text)s><input %(file)s>') empty_template = ('<input %(file)s>')
data_template = ('<div>'
' <input %(text)s>'
' <input type="checkbox" name="%(marker)s">Delete</input>'
'</div>'
'<input %(file)s>')
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id) kwargs.setdefault('id', field.id)
return HTMLString(self.template % { template = self.data_template if field.data else self.empty_template
return HTMLString(template % {
'text': html_params(type='text', 'text': html_params(type='text',
readonly='readonly',
value=kwargs.get('value')), value=kwargs.get('value')),
'file': html_params(type='file', 'file': html_params(type='file',
**kwargs) **kwargs),
'marker': '_%s-delete' % field.name
}) })
class ImageUploadInput(object):
"""
Renders a file input chooser field.
"""
empty_template = ('<input %(file)s>')
data_template = ('<div>'
' <img %(image)s>'
' <input type="checkbox" name="%(marker)s">Delete</input>'
'</div>'
'<input %(file)s>')
def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id)
args = {
'file': html_params(type='file',
**kwargs),
'marker': '_%s-delete' % field.name
}
value = kwargs.get('value')
if value and isinstance(value, string_types):
args['image'] = html_params(src=url_for(field.endpoint,
filename=field.thumnbnail_fn(value)))
template = self.data_template
else:
template = self.empty_template
return HTMLString(template % args)
# Fields # Fields
class FileUploadField(fields.TextField): class FileUploadField(fields.TextField):
""" """
...@@ -41,14 +99,13 @@ class FileUploadField(fields.TextField): ...@@ -41,14 +99,13 @@ class FileUploadField(fields.TextField):
widget = FileUploadInput() widget = FileUploadInput()
def __init__(self, label=None, validators=None, def __init__(self, label=None, validators=None,
path=None, namefn=None, endpoint='static', allowed_extensions=None, path=None, namegen=None, allowed_extensions=None,
**kwargs): **kwargs):
if not path: if not path:
raise ValueError('FileUploadField field requires target path.') raise ValueError('FileUploadField field requires target path.')
self.path = path self.path = path
self.namefn = namefn or namefn_keep_filename self.namegen = namegen or namegen_filename
self.endpoint = endpoint
self.allowed_extensions = allowed_extensions self.allowed_extensions = allowed_extensions
self._should_delete = False self._should_delete = False
...@@ -85,19 +142,95 @@ class FileUploadField(fields.TextField): ...@@ -85,19 +142,95 @@ class FileUploadField(fields.TextField):
if field: if field:
self._delete_file(field) self._delete_file(field)
filename = self.namefn(obj, self.data) filename = self.namegen(obj, self.data)
self._save_file(self.data, filename) self._save_file(self.data, filename)
setattr(obj, name, filename) setattr(obj, name, filename)
def _delete_file(self, filename): def _delete_file(self, filename):
path = op.join(self.path, filename) path = op.join(self.path, filename)
os.remove(path)
if op.exists(path):
os.remove(path)
def _save_file(self, data, filename): def _save_file(self, data, filename):
data.save(op.join(self.path, filename)) data.save(op.join(self.path, filename))
class ImageUploadField(FileUploadField):
widget = ImageUploadInput()
def __init__(self, label=None, validators=None,
path=None, namegen=None, allowed_extensions=None,
thumbgen=None, thumbnail_size=None, endpoint='static',
**kwargs):
# Check if PIL is installed
if Image is None:
raise Exception('PIL library was not found')
self.thumbnail_fn = thumbgen or thumbgen_filename
self.thumbnail_size = thumbnail_size
self.endpoint = endpoint
self.image = None
if not allowed_extensions:
allowed_extensions = ('gif', 'jpg', 'jpeg', 'png')
super(ImageUploadField, self).__init__(label, validators,
path=path,
namegen=namegen,
allowed_extensions=allowed_extensions,
**kwargs)
def pre_validate(self, form):
super(ImageUploadField, self).pre_validate(form)
if isinstance(self.data, FileStorage):
try:
self.image = Image.open(self.data)
except Exception as e:
raise ValidationError('Invalid image: %s' % e)
# Deletion
def _delete_file(self, filename):
super(ImageUploadField, self)._delete_file(filename)
self._delete_thumbnail(filename)
def _delete_thumbnail(self, filename):
path = op.join(self.path, self.thumbnail_fn(filename))
if op.exists(path):
os.remove(path)
# Saving
def _save_file(self, data, filename):
data.save(op.join(self.path, filename))
self._save_thumbnail(data, filename)
def _save_thumbnail(self, data, filename):
if self.image and self.thumbnail_size:
thumb = self.image
(width, height, force) = self.thumbnail_size
if self.image.size[0] > width or self.image.size[1] > height:
if force:
thumb = ImageOps.fit(self.image, (width, height), Image.ANTIALIAS)
else:
thumb = self.image.copy().thumbnail((width, height), Image.ANTIALIAS)
path = op.join(self.path, self.thumbnail_fn(filename))
with file(path, 'wb') as fp:
thumb.save(fp, 'JPEG')
# Helpers # Helpers
def namefn_keep_filename(obj, file_data): def namegen_filename(obj, file_data):
return secure_filename(file_data.filename) return secure_filename(file_data.filename)
def thumbgen_filename(filename):
name, ext = op.splitext(filename)
return '%s_thumb.jpg' % name
...@@ -15,10 +15,9 @@ def _create_temp(): ...@@ -15,10 +15,9 @@ def _create_temp():
return path return path
def _remove_testfiles(path): def safe_delete(path, name):
try: try:
os.remove(op.join(path, 'test1.txt')) os.remove(op.join(path, name))
os.remove(op.join(path, 'test2.txt'))
except: except:
pass pass
...@@ -28,6 +27,10 @@ def test_upload_field(): ...@@ -28,6 +27,10 @@ def test_upload_field():
path = _create_temp() path = _create_temp()
def _remove_testfiles():
safe_delete(path, 'test1.txt')
safe_delete(path, 'test2.txt')
class TestForm(form.BaseForm): class TestForm(form.BaseForm):
upload = form.FileUploadField('Upload', path=path) upload = form.FileUploadField('Upload', path=path)
...@@ -37,7 +40,7 @@ def test_upload_field(): ...@@ -37,7 +40,7 @@ def test_upload_field():
my_form = TestForm() my_form = TestForm()
eq_(my_form.upload.path, path) eq_(my_form.upload.path, path)
_remove_testfiles(path) _remove_testfiles()
dummy = Dummy() dummy = Dummy()
...@@ -72,3 +75,69 @@ def test_upload_field(): ...@@ -72,3 +75,69 @@ def test_upload_field():
my_form.populate_obj(dummy) my_form.populate_obj(dummy)
ok_(not op.exists(op.join(path, 'test2.txt'))) ok_(not op.exists(op.join(path, 'test2.txt')))
def test_image_upload_field():
app = Flask(__name__)
path = _create_temp()
def _remove_testimages():
safe_delete(path, 'test1.png')
safe_delete(path, 'test1_thumb.jpg')
safe_delete(path, 'test2.png')
safe_delete(path, 'test2_thumb.jpg')
class TestForm(form.BaseForm):
upload = form.ImageUploadField('Upload', path=path, thumbnail_size=(100, 100, True))
class Dummy(object):
pass
my_form = TestForm()
eq_(my_form.upload.path, path)
eq_(my_form.upload.endpoint, 'static')
_remove_testimages()
dummy = Dummy()
# Check upload
with file(op.join(op.dirname(__file__), 'data', 'copyleft.png'), 'rb') as fp:
with app.test_request_context(method='POST', data={'upload': (fp, 'test1.png')}):
my_form = TestForm(helpers.get_form_data())
ok_(my_form.validate())
my_form.populate_obj(dummy)
eq_(dummy.upload, 'test1.png')
ok_(op.exists(op.join(path, 'test1.png')))
ok_(op.exists(op.join(path, 'test1_thumb.jpg')))
# Check replace
with file(op.join(op.dirname(__file__), 'data', 'copyleft.png'), 'rb') as fp:
with app.test_request_context(method='POST', data={'upload': (fp, 'test2.png')}):
my_form = TestForm(helpers.get_form_data())
ok_(my_form.validate())
my_form.populate_obj(dummy)
eq_(dummy.upload, 'test2.png')
ok_(op.exists(op.join(path, 'test2.png')))
ok_(op.exists(op.join(path, 'test2_thumb.jpg')))
ok_(not op.exists(op.join(path, 'test1.png')))
ok_(not op.exists(op.join(path, 'test1_thumb.jpg')))
# Check delete
with app.test_request_context(method='POST', data={'_upload-delete': 'checked'}):
my_form = TestForm(helpers.get_form_data())
ok_(my_form.validate())
my_form.populate_obj(dummy)
ok_(not op.exists(op.join(path, 'test2.png')))
ok_(not op.exists(op.join(path, 'test2_thumb.jpg')))
...@@ -4,3 +4,4 @@ Flask-SQLAlchemy>=0.15 ...@@ -4,3 +4,4 @@ Flask-SQLAlchemy>=0.15
peewee peewee
wtf-peewee wtf-peewee
flask-mongoengine flask-mongoengine
pillow
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment