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 *
recursive-include flask_admin/templates *
recursive-include flask_admin/translations *
recursive-include flask_admin/tests *
recursive-exclude flask_admin *.pyc
This diff is collapsed.
......@@ -197,8 +197,8 @@ def build_sample_db():
if __name__ == '__main__':
# Build a sample db on the fly, if one does not exist yet.
app_dir = op.realpath(os.path.dirname(__file__))
database_path = op.join(app_dir, app.config['DATABASE_FILE'])
app_dir = os.path.realpath(os.path.dirname(__file__))
database_path = os.path.join(app_dir, app.config['DATABASE_FILE'])
if not os.path.exists(database_path):
build_sample_db()
......
......@@ -31,8 +31,8 @@ class User(db.Model):
username = db.Column(db.String(80), unique=True)
email = db.Column(db.String(120), unique=True)
# Required for administrative interface
def __str__(self):
# Required for administrative interface. For python 3 please use __str__ instead.
def __unicode__(self):
return self.username
......@@ -54,7 +54,7 @@ class Post(db.Model):
tags = db.relationship('Tag', secondary=post_tags_table)
def __str__(self):
def __unicode__(self):
return self.title
......@@ -62,7 +62,7 @@ class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64))
def __str__(self):
def __unicode__(self):
return self.name
......@@ -75,7 +75,7 @@ class UserInfo(db.Model):
user_id = db.Column(db.Integer(), db.ForeignKey(User.id))
user = db.relationship(User, backref='info')
def __str__(self):
def __unicode__(self):
return '%s - %s' % (self.key, self.value)
......@@ -85,7 +85,7 @@ class Tree(db.Model):
parent_id = db.Column(db.Integer, db.ForeignKey('tree.id'))
parent = db.relationship('Tree', remote_side=[id], backref='children')
def __str__(self):
def __unicode__(self):
return self.name
......
......@@ -32,7 +32,7 @@ if not PY2:
# Various tools
from functools import reduce
from urllib.parse import urljoin
from urllib.parse import urljoin, urlparse
else:
text_type = unicode
string_types = (str, unicode)
......@@ -50,7 +50,7 @@ else:
# Helpers
reduce = __builtins__['reduce'] if isinstance(__builtins__, dict) else __builtins__.reduce
from urlparse import urljoin
from urlparse import urljoin, urlparse
def with_metaclass(meta, *bases):
......
......@@ -155,6 +155,11 @@ class FileAdmin(BaseView, ActionsMixin):
Edit template
"""
upload_form = UploadForm
"""
Upload form class
"""
def __init__(self, base_path, base_url=None,
name=None, category=None, endpoint=None, url=None,
verify_path=True):
......@@ -285,7 +290,7 @@ class FileAdmin(BaseView, ActionsMixin):
"""
file_data.save(path)
def _get_dir_url(self, endpoint, path, **kwargs):
def _get_dir_url(self, endpoint, path=None, **kwargs):
"""
Return prettified URL
......@@ -410,6 +415,17 @@ class FileAdmin(BaseView, ActionsMixin):
"""
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('/b/<path:path>')
def index(self, path=None):
......@@ -423,7 +439,7 @@ class FileAdmin(BaseView, ActionsMixin):
base_path, directory, path = self._normalize_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'))
# Get directory listing
......@@ -486,21 +502,13 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(self._get_dir_url('.index', path))
if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.')))
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
form = UploadForm(self)
form = self.upload_form(self)
if helpers.validate_form_on_submit(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:
try:
self.save_file(filename, form.upload.data)
self.on_file_upload(directory, path, filename)
self._save_form_files(directory, path, form)
return redirect(self._get_dir_url('.index', path))
except Exception as ex:
flash(gettext('Failed to save file: %(error)s', error=ex))
......@@ -547,7 +555,7 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(dir_url)
if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.')))
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
form = NameForm(helpers.get_form_data())
......@@ -558,7 +566,7 @@ class FileAdmin(BaseView, ActionsMixin):
self.on_mkdir(directory, form.name.data)
return redirect(dir_url)
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,
form=form,
......@@ -584,7 +592,7 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(return_url)
if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.')))
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
if op.isdir(full_path):
......@@ -627,7 +635,7 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(return_url)
if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.')))
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
if not op.exists(full_path):
......@@ -672,8 +680,8 @@ class FileAdmin(BaseView, ActionsMixin):
base_path, full_path, path = self._normalize_path(path)
if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.')))
if not self.is_accessible_path(path) or not self.is_file_editable(path):
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
dir_url = self._get_dir_url('.index', os.path.dirname(path))
......
......@@ -106,9 +106,9 @@ class AdminModelConverter(ModelConverterBase):
kwargs['label'] = self._get_label(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())
elif prop.direction.name != 'MANYTOMANY':
else:
kwargs['validators'].append(validators.InputRequired())
# Contribute model-related parameters
......
......@@ -428,7 +428,10 @@ class ModelView(BaseModelView):
:returns:
``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):
"""
......
......@@ -177,7 +177,8 @@ class FileUploadField(fields.TextField):
return True
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):
if (self.data and
......@@ -208,6 +209,8 @@ class FileUploadField(fields.TextField):
filename = self.generate_name(obj, self.data)
filename = self._save_file(self.data, filename)
# update filename of FileStorage to our validated name
self.data.filename = filename
setattr(obj, name, filename)
......@@ -329,7 +332,7 @@ class ImageUploadField(FileUploadField):
"""
# Check if PIL is installed
if Image is None:
raise Exception('PIL library was not found')
raise ImportError('PIL library was not found')
self.max_size = max_size
self.thumbnail_fn = thumbgen or thumbgen_filename
......
......@@ -3,6 +3,9 @@ from jinja2 import contextfunction
from flask import g, request
from wtforms.validators import DataRequired, InputRequired
from flask.ext.admin._compat import urljoin, urlparse
from ._compat import string_types
......@@ -96,3 +99,17 @@ def prettify_class_name(name):
String to split
"""
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
from flask.ext.admin.form import BaseForm, FormOpts, rules
from flask.ext.admin.model import filters, typefmt
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._backwards import ObsoleteAttr
from flask.ext.admin._compat import iteritems, as_unicode
......@@ -1121,8 +1121,8 @@ class BaseModelView(BaseView, ActionsMixin):
"""
column_fmt = self.column_formatters.get(name)
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)
choices_map = self._column_choices_map.get(name, {})
......@@ -1250,7 +1250,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
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:
return redirect(return_url)
......@@ -1278,7 +1278,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
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:
return redirect(return_url)
......@@ -1316,7 +1316,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
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
if not self.can_delete:
......
......@@ -90,7 +90,7 @@
{% endif %}
</td>
<td>
{{ size }}
{{ size|filesizeformat }}
</td>
{% endif %}
{% endblock %}
......
......@@ -99,6 +99,7 @@
<input type="checkbox" name="rowid" class="action-checkbox" value="{{ get_pk_value(row) }}" title="{{ _gettext('Select record') }}" />
</td>
{% endif %}
{% block list_row_actions_column scoped %}
<td>
{% block list_row_actions scoped %}
{%- if admin_view.can_edit -%}
......@@ -118,6 +119,7 @@
{%- endif -%}
{% endblock %}
</td>
{% endblock %}
{% for c, name in list_columns %}
<td>{{ get_value(row, c) }}</td>
{% endfor %}
......
......@@ -833,3 +833,26 @@ def test_ajax_fk_multi():
ok_(mdl is not None)
ok_(mdl.model1 is not None)
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():
safe_delete(path, 'test2.png')
safe_delete(path, 'test2_thumb.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):
upload = form.ImageUploadField('Upload',
......@@ -204,6 +208,25 @@ def test_image_upload_field():
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():
app = Flask(__name__)
......
......@@ -254,6 +254,10 @@ msgstr "Создать"
msgid "Save and Add"
msgstr "Сохранить и Добавить"
#: ../flask_admin/templates/admin/model/edit.html:6
msgid "Save and Continue"
msgstr "Сохранить и Продолжить"
#: ../flask_admin/templates/admin/model/inline_form_list.html:24
msgid "Delete?"
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