Commit 216a66b6 authored by Serge S. Koval's avatar Serge S. Koval

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

parents e8833279 ed5943b8
...@@ -49,7 +49,7 @@ To run the examples on your local environment, one at a time, do something like: ...@@ -49,7 +49,7 @@ To run the examples on your local environment, one at a time, do something like:
Documentation Documentation
------------- -------------
Flask-Admin is extensively documented, you can find all of the documentation at `http://flask-admin.readthedocs.org/en/latest/ <http://flask-admin.readthedocs.org/en/latest/>`_. Flask-Admin is extensively documented, you can find all of the documentation at `https://flask-admin.readthedocs.io/en/latest/ <https://flask-admin.readthedocs.io/en/latest/>`_.
The docs are auto-generated from the *.rst* files in the */doc* folder. So if you come across any errors, or The docs are auto-generated from the *.rst* files in the */doc* folder. So if you come across any errors, or
if you think of anything else that should be included, then please make the changes and submit them as a *pull-request*. if you think of anything else that should be included, then please make the changes and submit them as a *pull-request*.
...@@ -75,7 +75,7 @@ Or alternatively, you can download the repository and install manually by doing: ...@@ -75,7 +75,7 @@ Or alternatively, you can download the repository and install manually by doing:
Tests Tests
----- -----
Test are run with *nose*. If you are not familiar with this package you can get some more info from `their website <http://nose.readthedocs.org/>`_. Test are run with *nose*. If you are not familiar with this package you can get some more info from `their website <https://nose.readthedocs.io/>`_.
To run the tests, from the project directory, simply:: To run the tests, from the project directory, simply::
......
...@@ -189,7 +189,7 @@ Managing Geographical Models ...@@ -189,7 +189,7 @@ Managing Geographical Models
If you want to store spatial information in a GIS database, Flask-Admin has If you want to store spatial information in a GIS database, Flask-Admin has
you covered. The GeoAlchemy backend extends the SQLAlchemy backend (just as you covered. The GeoAlchemy backend extends the SQLAlchemy backend (just as
`GeoAlchemy <http://geoalchemy-2.readthedocs.org/>`_ extends SQLAlchemy) to give you a pretty and functional map-based `GeoAlchemy <https://geoalchemy-2.readthedocs.io/>`_ extends SQLAlchemy) to give you a pretty and functional map-based
editor for your admin pages. editor for your admin pages.
Some notable features include: Some notable features include:
...@@ -200,7 +200,7 @@ Some notable features include: ...@@ -200,7 +200,7 @@ Some notable features include:
interactively using `Leaflet.Draw <https://github.com/Leaflet/Leaflet.draw>`_. interactively using `Leaflet.Draw <https://github.com/Leaflet/Leaflet.draw>`_.
- Graceful fallback: `GeoJSON <http://geojson.org/>`_ data can be edited in a ``<textarea>``, if the - Graceful fallback: `GeoJSON <http://geojson.org/>`_ data can be edited in a ``<textarea>``, if the
user has turned off Javascript. user has turned off Javascript.
- Works with a `Geometry <http://geoalchemy-2.readthedocs.org/en/latest/types.html#geoalchemy2.types.Geometry>`_ SQL field that is integrated with `Shapely <http://toblerity.org/shapely/>`_ objects. - Works with a `Geometry <https://geoalchemy-2.readthedocs.io/en/latest/types.html#geoalchemy2.types.Geometry>`_ SQL field that is integrated with `Shapely <http://toblerity.org/shapely/>`_ objects.
To get started, define some fields on your model using GeoAlchemy's *Geometry* To get started, define some fields on your model using GeoAlchemy's *Geometry*
field. Next, add model views to your interface using the ModelView class field. Next, add model views to your interface using the ModelView class
...@@ -387,7 +387,7 @@ Features: ...@@ -387,7 +387,7 @@ Features:
- GridFS support for file and image uploads - GridFS support for file and image uploads
In order to use MongoEngine integration, install the In order to use MongoEngine integration, install the
`Flask-MongoEngine <https://flask-mongoengine.readthedocs.org>`_ package. `Flask-MongoEngine <https://flask-mongoengine.readthedocs.io>`_ package.
Flask-Admin uses form scaffolding from it. Flask-Admin uses form scaffolding from it.
Known issues: Known issues:
...@@ -407,7 +407,7 @@ Features: ...@@ -407,7 +407,7 @@ Features:
- Inline editing of related models; - Inline editing of related models;
In order to use peewee integration, you need to install two additional Python In order to use peewee integration, you need to install two additional Python
packages: `peewee <https://peewee.readthedocs.org/>`_ and `wtf-peewee <https://github.com/coleifer/wtf-peewee/>`_. packages: `peewee <http://docs.peewee-orm.com/>`_ and `wtf-peewee <https://github.com/coleifer/wtf-peewee/>`_.
Known issues: Known issues:
......
...@@ -85,7 +85,7 @@ with your database models, and it doesn't require you to write any new view logi ...@@ -85,7 +85,7 @@ with your database models, and it doesn't require you to write any new view logi
template code. So it's great for when you're deploying something that's still template code. So it's great for when you're deploying something that's still
under development, before you want the whole world to see it. under development, before you want the whole world to see it.
Have a look at `Flask-BasicAuth <http://flask-basicauth.readthedocs.org/>`_ to see just how Have a look at `Flask-BasicAuth <https://flask-basicauth.readthedocs.io/>`_ to see just how
easy it is to put your whole application behind HTTP Basic Auth. easy it is to put your whole application behind HTTP Basic Auth.
Unfortunately, there is no easy way of applying HTTP Basic Auth just to your admin Unfortunately, there is no easy way of applying HTTP Basic Auth just to your admin
...@@ -96,7 +96,7 @@ Rolling Your Own ...@@ -96,7 +96,7 @@ Rolling Your Own
For a more flexible solution, Flask-Admin lets you define access control rules For a more flexible solution, Flask-Admin lets you define access control rules
on each of your admin view classes by simply overriding the `is_accessible` method. on each of your admin view classes by simply overriding the `is_accessible` method.
How you implement the logic is up to you, but if you were to use a low-level library like How you implement the logic is up to you, but if you were to use a low-level library like
`Flask-Login <https://flask-login.readthedocs.org/>`_, then restricting access `Flask-Login <https://flask-login.readthedocs.io/>`_, then restricting access
could be as simple as:: could be as simple as::
class MicroBlogModelView(sqla.ModelView): class MicroBlogModelView(sqla.ModelView):
......
...@@ -101,6 +101,7 @@ def security_context_processor(): ...@@ -101,6 +101,7 @@ def security_context_processor():
admin_base_template=admin.base_template, admin_base_template=admin.base_template,
admin_view=admin.index_view, admin_view=admin.index_view,
h=admin_helpers, h=admin_helpers,
get_url=url_for
) )
......
...@@ -21,7 +21,7 @@ db = SQLAlchemy(app) ...@@ -21,7 +21,7 @@ db = SQLAlchemy(app)
''' Define a wtforms widget and field. ''' Define a wtforms widget and field.
WTForms documentation on custom widgets: WTForms documentation on custom widgets:
http://wtforms.readthedocs.org/en/latest/widgets.html#custom-widgets https://wtforms.readthedocs.io/en/latest/widgets.html#custom-widgets
''' '''
class CKTextAreaWidget(widgets.TextArea): class CKTextAreaWidget(widgets.TextArea):
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
......
from wtforms.fields import TextField
from google.appengine.ext import ndb
import decimal
class GeoPtPropertyField(TextField):
def process_formdata(self, valuelist):
if valuelist:
try:
lat, lon = valuelist[0].split(',')
self.data = ndb.GeoPt(
decimal.Decimal(lat.strip()),
decimal.Decimal(lon.strip())
)
except (decimal.InvalidOperation, ValueError):
raise ValueError('Not a valid coordinate location')
from wtforms_appengine.ndb import ModelConverter
from .fields import GeoPtPropertyField
from flask_admin.model.form import converts
class AdminModelConverter(ModelConverter):
@converts('GeoPt')
def convert_GeoPtProperty(self, model, prop, kwargs):
"""Returns a form field for a ``ndb.GeoPtProperty``."""
return GeoPtPropertyField(**kwargs)
...@@ -7,6 +7,10 @@ from wtforms_appengine import ndb as wt_ndb ...@@ -7,6 +7,10 @@ from wtforms_appengine import ndb as wt_ndb
from google.appengine.ext import db from google.appengine.ext import db
from google.appengine.ext import ndb from google.appengine.ext import ndb
from flask_wtf import Form
from flask_admin.model.form import create_editable_list_form
from .form import AdminModelConverter
class NdbModelView(BaseModelView): class NdbModelView(BaseModelView):
""" """
AppEngine NDB model scaffolding. AppEngine NDB model scaffolding.
...@@ -31,10 +35,46 @@ class NdbModelView(BaseModelView): ...@@ -31,10 +35,46 @@ class NdbModelView(BaseModelView):
#TODO: implement #TODO: implement
pass pass
def scaffold_form(self): form_args = None
return wt_ndb.model_form(self.model())
def get_list(self, page, sort_field, sort_desc, search, filters): model_form_converter = AdminModelConverter
"""
Model form conversion class. Use this to implement custom field conversion logic.
For example::
class MyModelConverter(AdminModelConverter):
pass
class MyAdminView(ModelView):
model_form_converter = MyModelConverter
"""
def scaffold_form(self):
form_class = wt_ndb.model_form(
self.model(),
base_class=Form,
only=self.form_columns,
exclude=self.form_excluded_columns,
field_args=self.form_args,
converter=self.model_form_converter(),
)
return form_class
def scaffold_list_form(self, widget=None, validators=None):
form_class = wt_ndb.model_form(
self.model(),
base_class=Form,
only=self.column_editable_list,
field_args=self.form_args,
converter=self.model_form_converter(),
)
result = create_editable_list_form(Form, form_class, widget)
return result
def get_list(self, page, sort_field, sort_desc, search, filters,
page_size=None):
#TODO: implement filters (don't think search can work here) #TODO: implement filters (don't think search can work here)
q = self.model.query() q = self.model.query()
...@@ -45,7 +85,11 @@ class NdbModelView(BaseModelView): ...@@ -45,7 +85,11 @@ class NdbModelView(BaseModelView):
order_field = -order_field order_field = -order_field
q = q.order(order_field) q = q.order(order_field)
results = q.fetch(self.page_size, offset=page*self.page_size) if not page_size:
page_size = self.page_size
results = q.fetch(page_size, offset=page*page_size)
return q.count(), results return q.count(), results
def get_one(self, urlsafe_key): def get_one(self, urlsafe_key):
...@@ -56,30 +100,35 @@ class NdbModelView(BaseModelView): ...@@ -56,30 +100,35 @@ class NdbModelView(BaseModelView):
model = self.model() model = self.model()
form.populate_obj(model) form.populate_obj(model)
model.put() model.put()
return model
except Exception as ex: except Exception as ex:
if not self.handle_view_exception(ex): if not self.handle_view_exception(ex):
#flash(gettext('Failed to create record. %(error)s', #flash(gettext('Failed to create record. %(error)s',
# error=ex), 'error') # error=ex), 'error')
logging.exception('Failed to create record.') logging.exception('Failed to create record.')
return False return False
else:
self.after_model_change(form, model, True)
return model
def update_model(self, form, model): def update_model(self, form, model):
try: try:
form.populate_obj(model) form.populate_obj(model)
model.put() model.put()
return True
except Exception as ex: except Exception as ex:
if not self.handle_view_exception(ex): if not self.handle_view_exception(ex):
#flash(gettext('Failed to update record. %(error)s', #flash(gettext('Failed to update record. %(error)s',
# error=ex), 'error') # error=ex), 'error')
logging.exception('Failed to update record.') logging.exception('Failed to update record.')
return False return False
else:
self.after_model_change(form, model, False)
return True
def delete_model(self, model): def delete_model(self, model):
try: try:
model.key.delete() model.key.delete()
return True
except Exception as ex: except Exception as ex:
if not self.handle_view_exception(ex): if not self.handle_view_exception(ex):
#flash(gettext('Failed to delete record. %(error)s', #flash(gettext('Failed to delete record. %(error)s',
...@@ -87,6 +136,10 @@ class NdbModelView(BaseModelView): ...@@ -87,6 +136,10 @@ class NdbModelView(BaseModelView):
# 'error') # 'error')
logging.exception('Failed to delete record.') logging.exception('Failed to delete record.')
return False return False
else:
self.after_model_delete(model)
return True
class DbModelView(BaseModelView): class DbModelView(BaseModelView):
......
...@@ -557,7 +557,8 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -557,7 +557,8 @@ class InlineModelConverter(InlineModelConverterBase):
info = self.get_info(inline_model) info = self.get_info(inline_model)
# Find property from target model to current model # Find property from target model to current model
target_mapper = info.model._sa_class_manager.mapper # Use the base mapper to support inheritance
target_mapper = info.model._sa_class_manager.mapper.base_mapper
reverse_prop = None reverse_prop = None
......
...@@ -1117,7 +1117,15 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1117,7 +1117,15 @@ class BaseModelView(BaseView, ActionsMixin):
Filter instance Filter instance
""" """
if self.named_filter_urls: if self.named_filter_urls:
name = ('%s %s' % (flt.name, as_unicode(flt.operation()))).lower() operation = flt.operation()
try:
# get lazy string original value
operation = operation._args[0]
except AttributeError:
pass
name = ('%s %s' % (flt.name, as_unicode(operation))).lower()
name = filter_char_re.sub('', name) name = filter_char_re.sub('', name)
name = filter_compact_re.sub('_', name) name = filter_compact_re.sub('_', name)
return name return name
......
...@@ -98,7 +98,7 @@ class XEditableWidget(object): ...@@ -98,7 +98,7 @@ class XEditableWidget(object):
""" """
Return extra kwargs based on the field type. Return extra kwargs based on the field type.
""" """
if field.type == 'StringField': if field.type in ['StringField', 'TextField']:
kwargs['data-type'] = 'text' kwargs['data-type'] = 'text'
elif field.type == 'TextAreaField': elif field.type == 'TextAreaField':
kwargs['data-type'] = 'textarea' kwargs['data-type'] = 'textarea'
...@@ -111,7 +111,7 @@ class XEditableWidget(object): ...@@ -111,7 +111,7 @@ class XEditableWidget(object):
{'value': '1', 'text': gettext('Yes')} {'value': '1', 'text': gettext('Yes')}
]) ])
kwargs['data-role'] = 'x-editable-boolean' kwargs['data-role'] = 'x-editable-boolean'
elif field.type == 'Select2Field': elif field.type in ['Select2Field', 'SelectField']:
kwargs['data-type'] = 'select' kwargs['data-type'] = 'select'
choices = [{'value': x, 'text': y} for x, y in field.choices] choices = [{'value': x, 'text': y} for x, y in field.choices]
...@@ -142,7 +142,7 @@ class XEditableWidget(object): ...@@ -142,7 +142,7 @@ class XEditableWidget(object):
kwargs['data-type'] = 'number' kwargs['data-type'] = 'number'
kwargs['data-step'] = 'any' kwargs['data-step'] = 'any'
elif field.type in ['QuerySelectField', 'ModelSelectField', elif field.type in ['QuerySelectField', 'ModelSelectField',
'QuerySelectMultipleField']: 'QuerySelectMultipleField', 'KeyPropertyField']:
# QuerySelectField and ModelSelectField are for relations # QuerySelectField and ModelSelectField are for relations
kwargs['data-type'] = 'select' kwargs['data-type'] = 'select'
......
...@@ -1834,6 +1834,28 @@ def test_modelview_localization(): ...@@ -1834,6 +1834,28 @@ def test_modelview_localization():
for locale in locales: for locale in locales:
test_locale(locale) test_locale(locale)
def test_modelview_named_filter_localization():
app, db, admin = setup()
app.config['BABEL_DEFAULT_LOCALE'] = 'de'
Babel(app)
Model1, _ = create_models(db)
view = CustomModelView(
Model1, db.session,
named_filter_urls=True,
column_filters=['test1'],
)
filters = view.get_filters()
flt = filters[2]
with app.test_request_context():
flt_name = view.get_filter_arg(2, flt)
eq_('test1_equals', flt_name)
def test_custom_form_base(): def test_custom_form_base():
app, db, admin = setup() app, db, admin = setup()
......
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