Commit 6ed0fa36 authored by Serge S. Koval's avatar Serge S. Koval

Merge branch 'master' of github.com:mrjoes/flask-admin

parents 4e330484 5201bd1c
...@@ -56,6 +56,47 @@ Form Rendering Rule Description ...@@ -56,6 +56,47 @@ Form Rendering Rule Description
:class:`flask.ext.admin.form.rules.FieldSet` Renders form header and child rules :class:`flask.ext.admin.form.rules.FieldSet` Renders form header and child rules
======================================================= ======================================================== ======================================================= ========================================================
Enabling CSRF Validation
---------------
Flask-Admin does not use Flask-WTF Form class - it uses the wtforms Form class, which does not have CSRF validation.
Adding CSRF validation will require importing flask_wtf and overriding the :class:`flask.ext.admin.form.BaseForm` by using :attr:`flask.ext.admin.model.BaseModelView.form_base_class`::
import os
import flask
**import flask_wtf**
import flask_admin
import flask_sqlalchemy
from flask_admin.contrib.sqla import ModelView
DBFILE = 'app.db'
app = flask.Flask(__name__)
app.config['SECRET_KEY'] = 'Dnit7qz7mfcP0YuelDrF8vLFvk0snhwP'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + DBFILE
**app.config['CSRF_ENABLED'] = True**
**flask_wtf.CsrfProtect(app)**
db = flask_sqlalchemy.SQLAlchemy(app)
admin = flask_admin.Admin(app, name='Admin')
## Here is the fix:
class MyModelView(ModelView):
**form_base_class = flask_wtf.Form**
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String)
password = db.Column(db.String)
if not os.path.exists(DBFILE):
db.create_all()
## The subclass is used here:
admin.add_view( MyModelView(User, db.session, name='User') )
app.run(debug=True)
Further reading Further reading
--------------- ---------------
......
...@@ -292,8 +292,8 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)): ...@@ -292,8 +292,8 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
""" """
This method will be executed before calling any view method. This method will be executed before calling any view method.
By default, it will check if the admin class is accessible and if it is not it will It will execute the ``inaccessible_callback`` if the view is not
throw HTTP 404 error. accessible.
:param name: :param name:
View function name View function name
...@@ -301,7 +301,17 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)): ...@@ -301,7 +301,17 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
View function arguments View function arguments
""" """
if not self.is_accessible(): if not self.is_accessible():
return abort(403) return self.inaccessible_callback(name, **kwargs)
def inaccessible_callback(self, name, **kwargs):
"""
Handle the response to inaccessible views.
By default, it throw HTTP 403 error. Override this method to
customize the behaviour.
"""
return abort(403)
@property @property
def _debug(self): def _debug(self):
......
...@@ -423,12 +423,12 @@ class ModelView(BaseModelView): ...@@ -423,12 +423,12 @@ class ModelView(BaseModelView):
Verify if the provided column type is text-based. Verify if the provided column type is text-based.
:returns: :returns:
``True`` for ``String``, ``Unicode``, ``Text``, ``UnicodeText`` ``True`` for ``String``, ``Unicode``, ``Text``, ``UnicodeText``, ``varchar``
""" """
if name: if name:
name = name.lower() name = name.lower()
return name in ('string', 'unicode', 'text', 'unicodetext') return name in ('string', 'unicode', 'text', 'unicodetext', 'varchar')
def scaffold_filters(self, name): def scaffold_filters(self, name):
""" """
......
...@@ -7,6 +7,10 @@ from flask.ext.admin._compat import text_type, as_unicode ...@@ -7,6 +7,10 @@ from flask.ext.admin._compat import text_type, as_unicode
from . import widgets as admin_widgets from . import widgets as admin_widgets
"""
An understanding of WTForms's Custom Widgets is helpful for understanding this code: http://wtforms.simplecodes.com/docs/0.6.2/widgets.html#custom-widgets
"""
__all__ = ['DateTimeField', 'TimeField', 'Select2Field', 'Select2TagsField'] __all__ = ['DateTimeField', 'TimeField', 'Select2Field', 'Select2TagsField']
class DateTimeField(fields.DateTimeField): class DateTimeField(fields.DateTimeField):
...@@ -14,7 +18,7 @@ class DateTimeField(fields.DateTimeField): ...@@ -14,7 +18,7 @@ class DateTimeField(fields.DateTimeField):
Allows modifying the datetime format of a DateTimeField using form_args. Allows modifying the datetime format of a DateTimeField using form_args.
""" """
widget = admin_widgets.DateTimePickerWidget() widget = admin_widgets.DateTimePickerWidget()
def __init__(self, label=None, validators=None, format=None, widget_format=None, **kwargs): def __init__(self, label=None, validators=None, format=None, **kwargs):
""" """
Constructor Constructor
...@@ -22,19 +26,15 @@ class DateTimeField(fields.DateTimeField): ...@@ -22,19 +26,15 @@ class DateTimeField(fields.DateTimeField):
Label Label
:param validators: :param validators:
Field validators Field validators
:param format: :param format:
Format for text to date conversion. Defaults to '%Y-%m-%d %H:%M:%S' Format for text to date conversion. Defaults to '%Y-%m-%d %H:%M:%S'
:param widget_format:
Widget date format. Defaults to 'yyyy-mm-dd hh:ii:ss'
:param kwargs: :param kwargs:
Any additional parameters Any additional parameters
""" """
super(DateTimeField, self).__init__(label, validators, **kwargs) super(DateTimeField, self).__init__(label, validators, **kwargs)
self.format = format or '%Y-%m-%d %H:%M:%S'
self.widget_format = widget_format or 'yyyy-mm-dd hh:ii:ss'
self.format = format or '%Y-%m-%d %H:%M:%S'
class TimeField(fields.Field): class TimeField(fields.Field):
""" """
A text field which stores a `datetime.time` object. A text field which stores a `datetime.time` object.
...@@ -55,8 +55,6 @@ class TimeField(fields.Field): ...@@ -55,8 +55,6 @@ class TimeField(fields.Field):
Supported time formats, as a enumerable. Supported time formats, as a enumerable.
:param default_format: :param default_format:
Default time format. Defaults to '%H:%M:%S' Default time format. Defaults to '%H:%M:%S'
:param widget_format:
Widget date format. Defaults to 'hh:ii:ss'
:param kwargs: :param kwargs:
Any additional parameters Any additional parameters
""" """
...@@ -67,7 +65,6 @@ class TimeField(fields.Field): ...@@ -67,7 +65,6 @@ class TimeField(fields.Field):
'%I:%M:%S %p', '%I:%M %p') '%I:%M:%S %p', '%I:%M %p')
self.default_format = default_format or '%H:%M:%S' self.default_format = default_format or '%H:%M:%S'
self.widget_format = widget_format or 'hh:ii:ss'
def _value(self): def _value(self):
if self.raw_data: if self.raw_data:
......
...@@ -237,7 +237,7 @@ class FileUploadField(fields.TextField): ...@@ -237,7 +237,7 @@ class FileUploadField(fields.TextField):
def _save_file(self, data, filename): def _save_file(self, data, filename):
path = self._get_path(filename) path = self._get_path(filename)
if not op.exists(op.dirname(path)): if not op.exists(op.dirname(path)):
os.makedirs(os.path.dirname(path), self.permission) os.makedirs(os.path.dirname(path), self.permission | 0o111)
data.save(path) data.save(path)
......
...@@ -3,9 +3,7 @@ from flask.globals import _request_ctx_stack ...@@ -3,9 +3,7 @@ from flask.globals import _request_ctx_stack
from flask.ext.admin.babel import gettext, ngettext from flask.ext.admin.babel import gettext, ngettext
from flask.ext.admin import helpers as h from flask.ext.admin import helpers as h
__all__ = ['Select2Widget', 'DatePickerWidget', 'DateTimePickerWidget', 'RenderTemplateWidget', __all__ = ['Select2Widget', 'DatePickerWidget', 'DateTimePickerWidget', 'RenderTemplateWidget', 'Select2TagsWidget', ]
'Select2TagsWidget', ]
class Select2Widget(widgets.Select): class Select2Widget(widgets.Select):
""" """
...@@ -15,10 +13,9 @@ class Select2Widget(widgets.Select): ...@@ -15,10 +13,9 @@ class Select2Widget(widgets.Select):
work. work.
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', u'select2')
allow_blank = getattr(field, 'allow_blank', False) allow_blank = getattr(field, 'allow_blank', False)
kwargs['data-role'] = u'select2'
if allow_blank and not self.multiple: if allow_blank and not self.multiple:
kwargs['data-allow-blank'] = u'1' kwargs['data-allow-blank'] = u'1'
...@@ -30,8 +27,8 @@ class Select2TagsWidget(widgets.TextInput): ...@@ -30,8 +27,8 @@ class Select2TagsWidget(widgets.TextInput):
You must include select2.js, form.js and select2 stylesheet for it to work. You must include select2.js, form.js and select2 stylesheet for it to work.
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs['data-role'] = u'select2' kwargs.setdefault('data-role', u'select2')
kwargs['data-tags'] = u'1' kwargs.setdefault('data-tags', u'1')
return super(Select2TagsWidget, self).__call__(field, **kwargs) return super(Select2TagsWidget, self).__call__(field, **kwargs)
...@@ -43,9 +40,10 @@ class DatePickerWidget(widgets.TextInput): ...@@ -43,9 +40,10 @@ class DatePickerWidget(widgets.TextInput):
You must include bootstrap-datepicker.js and form.js for styling to work. You must include bootstrap-datepicker.js and form.js for styling to work.
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs['data-role'] = u'datepicker' kwargs.setdefault('data-role', u'datepicker')
kwargs['data-date-format'] = u'yyyy-mm-dd' kwargs.setdefault('data-date-format', u'yyyy-mm-dd')
kwargs['data-date-autoclose'] = u'true' kwargs.setdefault('data-date-autoclose', u'true')
self.date_format = kwargs['data-date-format']
return super(DatePickerWidget, self).__call__(field, **kwargs) return super(DatePickerWidget, self).__call__(field, **kwargs)
...@@ -56,11 +54,11 @@ class DateTimePickerWidget(widgets.TextInput): ...@@ -56,11 +54,11 @@ class DateTimePickerWidget(widgets.TextInput):
You must include bootstrap-datepicker.js and form.js for styling to work. You must include bootstrap-datepicker.js and form.js for styling to work.
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs['data-role'] = u'datetimepicker' kwargs.setdefault('data-role', u'datetimepicker')
kwargs['data-date-format'] = field.widget_format or u'yyyy-mm-dd hh:ii:ss' kwargs.setdefault('data-date-format', u'yyyy-mm-dd hh:ii:ss')
kwargs['data-date-autoclose'] = u'true' kwargs.setdefault('data-date-autoclose', u'true')
kwargs['data-date-today-btn'] = u'linked' kwargs.setdefault('data-date-today-btn', u'linked')
kwargs['data-date-today-highlight'] = u'true' kwargs.setdefault('data-date-today-highlight', u'true')
return super(DateTimePickerWidget, self).__call__(field, **kwargs) return super(DateTimePickerWidget, self).__call__(field, **kwargs)
...@@ -71,9 +69,9 @@ class TimePickerWidget(widgets.TextInput): ...@@ -71,9 +69,9 @@ class TimePickerWidget(widgets.TextInput):
You must include bootstrap-datepicker.js and form.js for styling to work. You must include bootstrap-datepicker.js and form.js for styling to work.
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs['data-role'] = u'timepicker' kwargs.setdefault('data-role', u'timepicker')
kwargs['data-date-format'] = field.widget_format or 'hh:ii:ss' kwargs.setdefault('data-date-format', u'hh:ii:ss')
kwargs['data-date-autoclose'] = u'true' kwargs.setdefault('data-date-autoclose', u'true')
return super(TimePickerWidget, self).__call__(field, **kwargs) return super(TimePickerWidget, self).__call__(field, **kwargs)
......
...@@ -365,6 +365,15 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -365,6 +365,15 @@ class BaseModelView(BaseView, ActionsMixin):
'style': 'color: black' 'style': 'color: black'
} }
} }
Note, changing the format of a DateTimeField will require changes to both form_widget_args and form_args::
form_args = dict(
start=dict(format='%Y-%m-%d %I:%M %p') # changes how the input is parsed by strptime (12 hour time)
)
form_widget_args = dict(
start={'data-date-format': u'yyyy-mm-dd HH:ii P', 'data-show-meridian': 'True'} # changes how the DateTimeField displays the time
)
""" """
form_extra_fields = None form_extra_fields = None
......
from nose.tools import ok_, eq_, raises from nose.tools import ok_, eq_, raises
from flask import Flask, request from flask import Flask, request, abort
from flask.views import MethodView from flask.views import MethodView
from flask.ext.admin import base from flask.ext.admin import base
...@@ -232,6 +232,20 @@ def test_permissions(): ...@@ -232,6 +232,20 @@ def test_permissions():
eq_(rv.status_code, 403) eq_(rv.status_code, 403)
def test_inaccessible_callback():
app = Flask(__name__)
admin = base.Admin(app)
view = MockView()
admin.add_view(view)
client = app.test_client()
view.allow_access = False
view.inaccessible_callback = lambda *args, **kwargs: abort(418)
rv = client.get('/admin/mockview/')
eq_(rv.status_code, 418)
def get_visibility(): def get_visibility():
app = Flask(__name__) app = Flask(__name__)
admin = base.Admin(app) admin = base.Admin(app)
......
This diff is collapsed.
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