Commit 4ec6473a authored by bryhoyt's avatar bryhoyt

Merge pull request #8 from mrjoes/master

Merge from central repo into bryhoyt's fork
parents 831a42c3 42d35970
...@@ -4,3 +4,4 @@ recursive-include flask_admin/static * ...@@ -4,3 +4,4 @@ recursive-include flask_admin/static *
recursive-include flask_admin/templates * recursive-include flask_admin/templates *
recursive-include flask_admin/translations * recursive-include flask_admin/translations *
recursive-include flask_admin/tests * recursive-include flask_admin/tests *
recursive-exclude flask_admin *.pyc
This diff is collapsed.
...@@ -197,8 +197,8 @@ def build_sample_db(): ...@@ -197,8 +197,8 @@ def build_sample_db():
if __name__ == '__main__': if __name__ == '__main__':
# Build a sample db on the fly, if one does not exist yet. # Build a sample db on the fly, if one does not exist yet.
app_dir = op.realpath(os.path.dirname(__file__)) app_dir = os.path.realpath(os.path.dirname(__file__))
database_path = op.join(app_dir, app.config['DATABASE_FILE']) database_path = os.path.join(app_dir, app.config['DATABASE_FILE'])
if not os.path.exists(database_path): if not os.path.exists(database_path):
build_sample_db() build_sample_db()
......
...@@ -31,8 +31,8 @@ class User(db.Model): ...@@ -31,8 +31,8 @@ class User(db.Model):
username = db.Column(db.String(80), unique=True) username = db.Column(db.String(80), unique=True)
email = db.Column(db.String(120), unique=True) email = db.Column(db.String(120), unique=True)
# Required for administrative interface # Required for administrative interface. For python 3 please use __str__ instead.
def __str__(self): def __unicode__(self):
return self.username return self.username
...@@ -54,7 +54,7 @@ class Post(db.Model): ...@@ -54,7 +54,7 @@ class Post(db.Model):
tags = db.relationship('Tag', secondary=post_tags_table) tags = db.relationship('Tag', secondary=post_tags_table)
def __str__(self): def __unicode__(self):
return self.title return self.title
...@@ -62,7 +62,7 @@ class Tag(db.Model): ...@@ -62,7 +62,7 @@ class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64)) name = db.Column(db.Unicode(64))
def __str__(self): def __unicode__(self):
return self.name return self.name
...@@ -75,7 +75,7 @@ class UserInfo(db.Model): ...@@ -75,7 +75,7 @@ class UserInfo(db.Model):
user_id = db.Column(db.Integer(), db.ForeignKey(User.id)) user_id = db.Column(db.Integer(), db.ForeignKey(User.id))
user = db.relationship(User, backref='info') user = db.relationship(User, backref='info')
def __str__(self): def __unicode__(self):
return '%s - %s' % (self.key, self.value) return '%s - %s' % (self.key, self.value)
...@@ -85,7 +85,7 @@ class Tree(db.Model): ...@@ -85,7 +85,7 @@ class Tree(db.Model):
parent_id = db.Column(db.Integer, db.ForeignKey('tree.id')) parent_id = db.Column(db.Integer, db.ForeignKey('tree.id'))
parent = db.relationship('Tree', remote_side=[id], backref='children') parent = db.relationship('Tree', remote_side=[id], backref='children')
def __str__(self): def __unicode__(self):
return self.name return self.name
......
...@@ -32,7 +32,7 @@ if not PY2: ...@@ -32,7 +32,7 @@ if not PY2:
# Various tools # Various tools
from functools import reduce from functools import reduce
from urllib.parse import urljoin from urllib.parse import urljoin, urlparse
else: else:
text_type = unicode text_type = unicode
string_types = (str, unicode) string_types = (str, unicode)
...@@ -50,7 +50,7 @@ else: ...@@ -50,7 +50,7 @@ else:
# Helpers # Helpers
reduce = __builtins__['reduce'] if isinstance(__builtins__, dict) else __builtins__.reduce reduce = __builtins__['reduce'] if isinstance(__builtins__, dict) else __builtins__.reduce
from urlparse import urljoin from urlparse import urljoin, urlparse
def with_metaclass(meta, *bases): def with_metaclass(meta, *bases):
......
...@@ -155,6 +155,11 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -155,6 +155,11 @@ class FileAdmin(BaseView, ActionsMixin):
Edit template Edit template
""" """
upload_form = UploadForm
"""
Upload form class
"""
def __init__(self, base_path, base_url=None, def __init__(self, base_path, base_url=None,
name=None, category=None, endpoint=None, url=None, name=None, category=None, endpoint=None, url=None,
verify_path=True): verify_path=True):
...@@ -285,7 +290,7 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -285,7 +290,7 @@ class FileAdmin(BaseView, ActionsMixin):
""" """
file_data.save(path) file_data.save(path)
def _get_dir_url(self, endpoint, path, **kwargs): def _get_dir_url(self, endpoint, path=None, **kwargs):
""" """
Return prettified URL Return prettified URL
...@@ -410,6 +415,17 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -410,6 +415,17 @@ class FileAdmin(BaseView, ActionsMixin):
""" """
pass pass
def _save_form_files(self, directory, path, form):
filename = op.join(directory,
secure_filename(form.upload.data.filename))
if op.exists(filename):
flash(gettext('File "%(name)s" already exists.', name=filename),
'error')
else:
self.save_file(filename, form.upload.data)
self.on_file_upload(directory, path, filename)
@expose('/') @expose('/')
@expose('/b/<path:path>') @expose('/b/<path:path>')
def index(self, path=None): def index(self, path=None):
...@@ -423,7 +439,7 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -423,7 +439,7 @@ class FileAdmin(BaseView, ActionsMixin):
base_path, directory, path = self._normalize_path(path) base_path, directory, path = self._normalize_path(path)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.'))) flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
# Get directory listing # Get directory listing
...@@ -486,24 +502,16 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -486,24 +502,16 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(self._get_dir_url('.index', path)) return redirect(self._get_dir_url('.index', path))
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.'))) flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
form = UploadForm(self) form = self.upload_form(self)
if helpers.validate_form_on_submit(form): if helpers.validate_form_on_submit(form):
filename = op.join(directory, try:
secure_filename(form.upload.data.filename)) self._save_form_files(directory, path, form)
return redirect(self._get_dir_url('.index', path))
if op.exists(filename): except Exception as ex:
flash(gettext('File "%(name)s" already exists.', name=filename), flash(gettext('Failed to save file: %(error)s', error=ex))
'error')
else:
try:
self.save_file(filename, form.upload.data)
self.on_file_upload(directory, path, filename)
return redirect(self._get_dir_url('.index', path))
except Exception as ex:
flash(gettext('Failed to save file: %(error)s', error=ex))
return self.render(self.upload_template, form=form) return self.render(self.upload_template, form=form)
...@@ -547,7 +555,7 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -547,7 +555,7 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(dir_url) return redirect(dir_url)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.'))) flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
form = NameForm(helpers.get_form_data()) form = NameForm(helpers.get_form_data())
...@@ -558,7 +566,7 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -558,7 +566,7 @@ class FileAdmin(BaseView, ActionsMixin):
self.on_mkdir(directory, form.name.data) self.on_mkdir(directory, form.name.data)
return redirect(dir_url) return redirect(dir_url)
except Exception as ex: except Exception as ex:
flash(gettext('Failed to create directory: %(error)s', ex), 'error') flash(gettext('Failed to create directory: %(error)s', error=ex), 'error')
return self.render(self.mkdir_template, return self.render(self.mkdir_template,
form=form, form=form,
...@@ -584,7 +592,7 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -584,7 +592,7 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(return_url) return redirect(return_url)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.'))) flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
if op.isdir(full_path): if op.isdir(full_path):
...@@ -627,7 +635,7 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -627,7 +635,7 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(return_url) return redirect(return_url)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.'))) flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
if not op.exists(full_path): if not op.exists(full_path):
...@@ -672,8 +680,8 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -672,8 +680,8 @@ class FileAdmin(BaseView, ActionsMixin):
base_path, full_path, path = self._normalize_path(path) base_path, full_path, path = self._normalize_path(path)
if not self.is_accessible_path(path): if not self.is_accessible_path(path) or not self.is_file_editable(path):
flash(gettext(gettext('Permission denied.'))) flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
dir_url = self._get_dir_url('.index', os.path.dirname(path)) dir_url = self._get_dir_url('.index', os.path.dirname(path))
......
...@@ -106,9 +106,9 @@ class AdminModelConverter(ModelConverterBase): ...@@ -106,9 +106,9 @@ class AdminModelConverter(ModelConverterBase):
kwargs['label'] = self._get_label(prop.key, kwargs) kwargs['label'] = self._get_label(prop.key, kwargs)
kwargs['description'] = self._get_description(prop.key, kwargs) kwargs['description'] = self._get_description(prop.key, kwargs)
if column.nullable: if column.nullable or prop.direction.name != 'MANYTOONE':
kwargs['validators'].append(validators.Optional()) kwargs['validators'].append(validators.Optional())
elif prop.direction.name != 'MANYTOMANY': else:
kwargs['validators'].append(validators.InputRequired()) kwargs['validators'].append(validators.InputRequired())
# Contribute model-related parameters # Contribute model-related parameters
......
...@@ -428,7 +428,10 @@ class ModelView(BaseModelView): ...@@ -428,7 +428,10 @@ class ModelView(BaseModelView):
:returns: :returns:
``True`` for ``String``, ``Unicode``, ``Text``, ``UnicodeText`` ``True`` for ``String``, ``Unicode``, ``Text``, ``UnicodeText``
""" """
return name in ('String', 'Unicode', 'Text', 'UnicodeText') if name:
name = name.lower()
return name in ('string', 'unicode', 'text', 'unicodetext')
def scaffold_filters(self, name): def scaffold_filters(self, name):
""" """
......
...@@ -177,7 +177,8 @@ class FileUploadField(fields.TextField): ...@@ -177,7 +177,8 @@ class FileUploadField(fields.TextField):
return True return True
return ('.' in filename and return ('.' in filename and
filename.rsplit('.', 1)[1] in self.allowed_extensions) filename.rsplit('.', 1)[1].lower() in
map(str.lower, self.allowed_extensions))
def pre_validate(self, form): def pre_validate(self, form):
if (self.data and if (self.data and
...@@ -208,6 +209,8 @@ class FileUploadField(fields.TextField): ...@@ -208,6 +209,8 @@ class FileUploadField(fields.TextField):
filename = self.generate_name(obj, self.data) filename = self.generate_name(obj, self.data)
filename = self._save_file(self.data, filename) filename = self._save_file(self.data, filename)
# update filename of FileStorage to our validated name
self.data.filename = filename
setattr(obj, name, filename) setattr(obj, name, filename)
...@@ -329,7 +332,7 @@ class ImageUploadField(FileUploadField): ...@@ -329,7 +332,7 @@ class ImageUploadField(FileUploadField):
""" """
# Check if PIL is installed # Check if PIL is installed
if Image is None: if Image is None:
raise Exception('PIL library was not found') raise ImportError('PIL library was not found')
self.max_size = max_size self.max_size = max_size
self.thumbnail_fn = thumbgen or thumbgen_filename self.thumbnail_fn = thumbgen or thumbgen_filename
......
...@@ -3,6 +3,9 @@ from jinja2 import contextfunction ...@@ -3,6 +3,9 @@ from jinja2 import contextfunction
from flask import g, request from flask import g, request
from wtforms.validators import DataRequired, InputRequired from wtforms.validators import DataRequired, InputRequired
from flask.ext.admin._compat import urljoin, urlparse
from ._compat import string_types from ._compat import string_types
...@@ -96,3 +99,17 @@ def prettify_class_name(name): ...@@ -96,3 +99,17 @@ def prettify_class_name(name):
String to split String to split
""" """
return sub(r'(?<=.)([A-Z])', r' \1', name) return sub(r'(?<=.)([A-Z])', r' \1', name)
def is_safe_url(target):
ref_url = urlparse(request.host_url)
test_url = urlparse(urljoin(request.host_url, target))
return (test_url.scheme in ('http', 'https') and
ref_url.netloc == test_url.netloc)
def get_redirect_target(param_name='url'):
target = request.values.get(param_name)
if target and is_safe_url(target):
return target
...@@ -11,7 +11,7 @@ from flask.ext.admin.base import BaseView, expose ...@@ -11,7 +11,7 @@ from flask.ext.admin.base import BaseView, expose
from flask.ext.admin.form import BaseForm, FormOpts, rules from flask.ext.admin.form import BaseForm, FormOpts, rules
from flask.ext.admin.model import filters, typefmt from flask.ext.admin.model import filters, typefmt
from flask.ext.admin.actions import ActionsMixin from flask.ext.admin.actions import ActionsMixin
from flask.ext.admin.helpers import get_form_data, validate_form_on_submit from flask.ext.admin.helpers import get_form_data, validate_form_on_submit, get_redirect_target
from flask.ext.admin.tools import rec_getattr from flask.ext.admin.tools import rec_getattr
from flask.ext.admin._backwards import ObsoleteAttr from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin._compat import iteritems, as_unicode from flask.ext.admin._compat import iteritems, as_unicode
...@@ -1121,9 +1121,9 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1121,9 +1121,9 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
column_fmt = self.column_formatters.get(name) column_fmt = self.column_formatters.get(name)
if column_fmt is not None: if column_fmt is not None:
return column_fmt(self, context, model, name) value = column_fmt(self, context, model, name)
else:
value = self._get_field_value(model, name) value = self._get_field_value(model, name)
choices_map = self._column_choices_map.get(name, {}) choices_map = self._column_choices_map.get(name, {})
if choices_map: if choices_map:
...@@ -1250,7 +1250,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1250,7 +1250,7 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
Create model view Create model view
""" """
return_url = request.args.get('url') or url_for('.index_view') return_url = get_redirect_target() or url_for('.index_view')
if not self.can_create: if not self.can_create:
return redirect(return_url) return redirect(return_url)
...@@ -1278,7 +1278,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1278,7 +1278,7 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
Edit model view Edit model view
""" """
return_url = request.args.get('url') or url_for('.index_view') return_url = get_redirect_target() or url_for('.index_view')
if not self.can_edit: if not self.can_edit:
return redirect(return_url) return redirect(return_url)
...@@ -1316,7 +1316,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1316,7 +1316,7 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
Delete model view. Only POST method is allowed. Delete model view. Only POST method is allowed.
""" """
return_url = request.args.get('url') or url_for('.index_view') return_url = get_redirect_target() or url_for('.index_view')
# TODO: Use post # TODO: Use post
if not self.can_delete: if not self.can_delete:
......
...@@ -90,7 +90,7 @@ ...@@ -90,7 +90,7 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{{ size }} {{ size|filesizeformat }}
</td> </td>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
......
...@@ -99,6 +99,7 @@ ...@@ -99,6 +99,7 @@
<input type="checkbox" name="rowid" class="action-checkbox" value="{{ get_pk_value(row) }}" title="{{ _gettext('Select record') }}" /> <input type="checkbox" name="rowid" class="action-checkbox" value="{{ get_pk_value(row) }}" title="{{ _gettext('Select record') }}" />
</td> </td>
{% endif %} {% endif %}
{% block list_row_actions_column scoped %}
<td> <td>
{% block list_row_actions scoped %} {% block list_row_actions scoped %}
{%- if admin_view.can_edit -%} {%- if admin_view.can_edit -%}
...@@ -118,6 +119,7 @@ ...@@ -118,6 +119,7 @@
{%- endif -%} {%- endif -%}
{% endblock %} {% endblock %}
</td> </td>
{% endblock %}
{% for c, name in list_columns %} {% for c, name in list_columns %}
<td>{{ get_value(row, c) }}</td> <td>{{ get_value(row, c) }}</td>
{% endfor %} {% endfor %}
......
...@@ -833,3 +833,26 @@ def test_ajax_fk_multi(): ...@@ -833,3 +833,26 @@ def test_ajax_fk_multi():
ok_(mdl is not None) ok_(mdl is not None)
ok_(mdl.model1 is not None) ok_(mdl.model1 is not None)
eq_(len(mdl.model1), 1) eq_(len(mdl.model1), 1)
def test_safe_redirect():
app, db, admin = setup()
Model1, _ = create_models(db)
db.create_all()
view = CustomModelView(Model1, db.session)
admin.add_view(view)
client = app.test_client()
rv = client.post('/admin/model1view/new/?url=http://localhost/admin/model2view/',
data=dict(test1='test1large', test2='test2'))
eq_(rv.status_code, 302)
eq_(rv.location, 'http://localhost/admin/model2view/')
rv = client.post('/admin/model1view/new/?url=http://google.com/evil/',
data=dict(test1='test1large', test2='test2'))
eq_(rv.status_code, 302)
eq_(rv.location, 'http://localhost/admin/model1view/')
...@@ -95,6 +95,10 @@ def test_image_upload_field(): ...@@ -95,6 +95,10 @@ def test_image_upload_field():
safe_delete(path, 'test2.png') safe_delete(path, 'test2.png')
safe_delete(path, 'test2_thumb.jpg') safe_delete(path, 'test2_thumb.jpg')
safe_delete(path, 'test1.jpg') safe_delete(path, 'test1.jpg')
safe_delete(path, 'test1.jpeg')
safe_delete(path, 'test1.gif')
safe_delete(path, 'test1.png')
safe_delete(path, 'test1.tiff')
class TestForm(form.BaseForm): class TestForm(form.BaseForm):
upload = form.ImageUploadField('Upload', upload = form.ImageUploadField('Upload',
...@@ -204,6 +208,25 @@ def test_image_upload_field(): ...@@ -204,6 +208,25 @@ def test_image_upload_field():
ok_(op.exists(op.join(path, 'test1.jpg'))) ok_(op.exists(op.join(path, 'test1.jpg')))
# check allowed extensions
for extension in ('gif', 'jpg', 'jpeg', 'png', 'tiff'):
filename = 'copyleft.' + extension
filepath = op.join(op.dirname(__file__), 'data', filename)
with open(filepath, 'rb') as fp:
with app.test_request_context(method='POST', data={'upload': (fp, filename)}):
my_form = TestNoResizeForm(helpers.get_form_data())
ok_(my_form.validate())
my_form.populate_obj(dummy)
eq_(dummy.upload, my_form.upload.data.filename)
# check case-sensitivity for extensions
filename = op.join(op.dirname(__file__), 'data', 'copyleft.jpg')
with open(filename, 'rb') as fp:
with app.test_request_context(method='POST', data={'upload': (fp, 'copyleft.JPG')}):
my_form = TestNoResizeForm(helpers.get_form_data())
ok_(my_form.validate())
def test_relative_path(): def test_relative_path():
app = Flask(__name__) app = Flask(__name__)
......
...@@ -254,6 +254,10 @@ msgstr "Создать" ...@@ -254,6 +254,10 @@ msgstr "Создать"
msgid "Save and Add" msgid "Save and Add"
msgstr "Сохранить и Добавить" msgstr "Сохранить и Добавить"
#: ../flask_admin/templates/admin/model/edit.html:6
msgid "Save and Continue"
msgstr "Сохранить и Продолжить"
#: ../flask_admin/templates/admin/model/inline_form_list.html:24 #: ../flask_admin/templates/admin/model/inline_form_list.html:24
msgid "Delete?" msgid "Delete?"
msgstr "Удалить?" msgstr "Удалить?"
......
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