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

Added relative_path to upload fields. Fixes #269

parent 99f09f5b
import os import os
import os.path as op import os.path as op
import logging from urlparse import urljoin
from flask import url_for from flask import url_for
...@@ -86,8 +86,12 @@ class ImageUploadInput(object): ...@@ -86,8 +86,12 @@ class ImageUploadInput(object):
} }
if field.data and isinstance(field.data, string_types): if field.data and isinstance(field.data, string_types):
args['image'] = html_params(src=url_for(field.endpoint, if field.thumbnail_size:
filename=field.thumbnail_fn(field.data))) url = url_for(field.endpoint, filename=field.thumbnail_fn(field.data))
else:
url = url_for(field.endpoint, filename=field.data)
args['image'] = html_params(src=url)
template = self.data_template template = self.data_template
else: else:
...@@ -107,7 +111,8 @@ class FileUploadField(fields.TextField): ...@@ -107,7 +111,8 @@ class FileUploadField(fields.TextField):
widget = FileUploadInput() widget = FileUploadInput()
def __init__(self, label=None, validators=None, def __init__(self, label=None, validators=None,
path=None, namegen=None, allowed_extensions=None, base_path=None, relative_path=None,
namegen=None, allowed_extensions=None,
**kwargs): **kwargs):
""" """
Constructor. Constructor.
...@@ -116,8 +121,12 @@ class FileUploadField(fields.TextField): ...@@ -116,8 +121,12 @@ class FileUploadField(fields.TextField):
Display label Display label
:param validators: :param validators:
Validators Validators
:param path: :param base_path:
Full path to the directory which will store files Absolute path to the directory which will store files
:param relative_path:
Relative path from the directory. Will be prepended to the file name for uploaded files.
Flask-Admin uses `urlparse.urljoin` to generate resulting filename, so make sure you have
trailing slash.
:param namegen: :param namegen:
Function that will generate filename from the model and uploaded file object. Function that will generate filename from the model and uploaded file object.
Please note, that model is "dirty" model object, before it was committed to database. Please note, that model is "dirty" model object, before it was committed to database.
...@@ -136,10 +145,12 @@ class FileUploadField(fields.TextField): ...@@ -136,10 +145,12 @@ class FileUploadField(fields.TextField):
:param allowed_extensions: :param allowed_extensions:
List of allowed extensions. If not provided, will allow any file. List of allowed extensions. If not provided, will allow any file.
""" """
if not path: if not base_path:
raise ValueError('FileUploadField field requires target path.') raise ValueError('FileUploadField field requires target path.')
self.path = path self.base_path = base_path
self.relative_path = relative_path
self.namegen = namegen or namegen_filename self.namegen = namegen or namegen_filename
self.allowed_extensions = allowed_extensions self.allowed_extensions = allowed_extensions
self._should_delete = False self._should_delete = False
...@@ -186,19 +197,31 @@ class FileUploadField(fields.TextField): ...@@ -186,19 +197,31 @@ class FileUploadField(fields.TextField):
if field: if field:
self._delete_file(field) self._delete_file(field)
filename = self.namegen(obj, self.data) filename = self.generate_name(obj, self.data)
self._save_file(self.data, filename) self._save_file(self.data, filename)
setattr(obj, name, filename) setattr(obj, name, filename)
def generate_name(self, obj, file_data):
filename = self.namegen(obj, file_data)
if not self.relative_path:
return filename
return urljoin(self.relative_path, filename)
def _get_path(self, filename):
return op.join(self.base_path, filename)
def _delete_file(self, filename): def _delete_file(self, filename):
path = op.join(self.path, filename) path = self._get_path(filename)
if op.exists(path): if op.exists(path):
os.remove(path) os.remove(path)
def _save_file(self, data, filename): def _save_file(self, data, filename):
data.save(op.join(self.path, filename)) path = self._get_path(filename)
data.save(path)
class ImageUploadField(FileUploadField): class ImageUploadField(FileUploadField):
...@@ -212,7 +235,8 @@ class ImageUploadField(FileUploadField): ...@@ -212,7 +235,8 @@ class ImageUploadField(FileUploadField):
widget = ImageUploadInput() widget = ImageUploadInput()
def __init__(self, label=None, validators=None, def __init__(self, label=None, validators=None,
path=None, namegen=None, allowed_extensions=None, base_path=None, relative_path=None,
namegen=None, allowed_extensions=None,
thumbgen=None, thumbnail_size=None, endpoint='static', thumbgen=None, thumbnail_size=None, endpoint='static',
**kwargs): **kwargs):
""" """
...@@ -222,8 +246,12 @@ class ImageUploadField(FileUploadField): ...@@ -222,8 +246,12 @@ class ImageUploadField(FileUploadField):
Display label Display label
:param validators: :param validators:
Validators Validators
:param path: :param base_path:
Full path to the directory which will store files Absolute path to the directory which will store files
:param relative_path:
Relative path from the directory. Will be prepended to the file name for uploaded files.
Flask-Admin uses `urlparse.urljoin` to generate resulting filename, so make sure you have
trailing slash.
:param namegen: :param namegen:
Function that will generate filename from the model and uploaded file object. Function that will generate filename from the model and uploaded file object.
Please note, that model is "dirty" model object, before it was committed to database. Please note, that model is "dirty" model object, before it was committed to database.
...@@ -277,7 +305,8 @@ class ImageUploadField(FileUploadField): ...@@ -277,7 +305,8 @@ class ImageUploadField(FileUploadField):
allowed_extensions = ('gif', 'jpg', 'jpeg', 'png') allowed_extensions = ('gif', 'jpg', 'jpeg', 'png')
super(ImageUploadField, self).__init__(label, validators, super(ImageUploadField, self).__init__(label, validators,
path=path, base_path=base_path,
relative_path=relative_path,
namegen=namegen, namegen=namegen,
allowed_extensions=allowed_extensions, allowed_extensions=allowed_extensions,
**kwargs) **kwargs)
...@@ -298,14 +327,14 @@ class ImageUploadField(FileUploadField): ...@@ -298,14 +327,14 @@ class ImageUploadField(FileUploadField):
self._delete_thumbnail(filename) self._delete_thumbnail(filename)
def _delete_thumbnail(self, filename): def _delete_thumbnail(self, filename):
path = op.join(self.path, self.thumbnail_fn(filename)) path = self._get_path(self.thumbnail_fn(filename))
if op.exists(path): if op.exists(path):
os.remove(path) os.remove(path)
# Saving # Saving
def _save_file(self, data, filename): def _save_file(self, data, filename):
data.save(op.join(self.path, filename)) data.save(self._get_path(filename))
self._save_thumbnail(data, filename) self._save_thumbnail(data, filename)
...@@ -321,7 +350,7 @@ class ImageUploadField(FileUploadField): ...@@ -321,7 +350,7 @@ class ImageUploadField(FileUploadField):
else: else:
thumb = self.image.copy().thumbnail((width, height), Image.ANTIALIAS) thumb = self.image.copy().thumbnail((width, height), Image.ANTIALIAS)
path = op.join(self.path, self.thumbnail_fn(filename)) path = self._get_path(self.thumbnail_fn(filename))
with open(path, 'wb') as fp: with open(path, 'wb') as fp:
thumb.save(fp, 'JPEG') thumb.save(fp, 'JPEG')
......
...@@ -5,7 +5,7 @@ from io import BytesIO ...@@ -5,7 +5,7 @@ from io import BytesIO
from nose.tools import eq_, ok_ from nose.tools import eq_, ok_
from flask import Flask from flask import Flask, url_for
from flask.ext.admin import form, helpers from flask.ext.admin import form, helpers
...@@ -13,6 +13,11 @@ def _create_temp(): ...@@ -13,6 +13,11 @@ def _create_temp():
path = op.join(op.dirname(__file__), 'tmp') path = op.join(op.dirname(__file__), 'tmp')
if not op.exists(path): if not op.exists(path):
os.mkdir(path) os.mkdir(path)
inner = op.join(path, 'inner')
if not op.exists(inner):
os.mkdir(inner)
return path return path
...@@ -33,13 +38,13 @@ def test_upload_field(): ...@@ -33,13 +38,13 @@ def test_upload_field():
safe_delete(path, 'test2.txt') safe_delete(path, 'test2.txt')
class TestForm(form.BaseForm): class TestForm(form.BaseForm):
upload = form.FileUploadField('Upload', path=path) upload = form.FileUploadField('Upload', base_path=path)
class Dummy(object): class Dummy(object):
pass pass
my_form = TestForm() my_form = TestForm()
eq_(my_form.upload.path, path) eq_(my_form.upload.base_path, path)
_remove_testfiles() _remove_testfiles()
...@@ -91,16 +96,18 @@ def test_image_upload_field(): ...@@ -91,16 +96,18 @@ def test_image_upload_field():
safe_delete(path, 'test2_thumb.jpg') safe_delete(path, 'test2_thumb.jpg')
class TestForm(form.BaseForm): class TestForm(form.BaseForm):
upload = form.ImageUploadField('Upload', path=path, thumbnail_size=(100, 100, True)) upload = form.ImageUploadField('Upload',
base_path=path,
thumbnail_size=(100, 100, True))
class TestNoResizeForm(form.BaseForm): class TestNoResizeForm(form.BaseForm):
upload = form.ImageUploadField('Upload', path=path) upload = form.ImageUploadField('Upload', base_path=path, endpoint='test')
class Dummy(object): class Dummy(object):
pass pass
my_form = TestForm() my_form = TestForm()
eq_(my_form.upload.path, path) eq_(my_form.upload.base_path, path)
eq_(my_form.upload.endpoint, 'static') eq_(my_form.upload.endpoint, 'static')
_remove_testimages() _remove_testimages()
...@@ -162,3 +169,39 @@ def test_image_upload_field(): ...@@ -162,3 +169,39 @@ def test_image_upload_field():
eq_(dummy.upload, 'test1.png') eq_(dummy.upload, 'test1.png')
ok_(op.exists(op.join(path, 'test1.png'))) ok_(op.exists(op.join(path, 'test1.png')))
ok_(not op.exists(op.join(path, 'test1_thumb.jpg'))) ok_(not op.exists(op.join(path, 'test1_thumb.jpg')))
def test_relative_path():
app = Flask(__name__)
path = _create_temp()
def _remove_testfiles():
safe_delete(path, 'test1.txt')
class TestForm(form.BaseForm):
upload = form.FileUploadField('Upload', base_path=path, relative_path='inner/')
class Dummy(object):
pass
my_form = TestForm()
eq_(my_form.upload.base_path, path)
eq_(my_form.upload.relative_path, 'inner/')
_remove_testfiles()
dummy = Dummy()
# Check upload
with app.test_request_context(method='POST', data={'upload': (BytesIO(b'Hello World 1'), 'test1.txt')}):
my_form = TestForm(helpers.get_form_data())
ok_(my_form.validate())
my_form.populate_obj(dummy)
eq_(dummy.upload, 'inner/test1.txt')
ok_(op.exists(op.join(path, 'inner/test1.txt')))
eq_(url_for('static', filename=dummy.upload), '/static/inner/test1.txt')
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