Commit 30f15ba5 authored by PJ Janse van Rensburg's avatar PJ Janse van Rensburg

Merge branch 'master' into upgrade-leaflet

parents 186ea42d 23dddb0e
...@@ -26,3 +26,4 @@ examples/appengine/lib ...@@ -26,3 +26,4 @@ examples/appengine/lib
env env
*.egg *.egg
.eggs .eggs
.tox/
sudo: false sudo: false
language: python language: python
python: matrix:
- "2.6" include:
- "2.7" - python: 2.6
- "3.3" env: TOX_ENV=py26-WTForms1
- "3.4" - python: 2.6
- "3.5" env: TOX_ENV=py26-WTForms2
- python: 2.7
env: env: TOX_ENV=py27-WTForms1
- WTFORMS_VERSION=1 - python: 2.7
- WTFORMS_VERSION=2 env: TOX_ENV=py27-WTForms2
- python: 2.7
env: TOX_ENV=flake8
- python: 3.3
env: TOX_ENV=py33-WTForms1
- python: 3.3
env: TOX_ENV=py33-WTForms2
- python: 3.4
env: TOX_ENV=py34-WTForms1
- python: 3.4
env: TOX_ENV=py34-WTForms2
- python: 3.5
env: TOX_ENV=py35-WTForms1
- python: 3.5
env: TOX_ENV=py35-WTForms2
- python: 3.6
env: TOX_ENV=py36-WTForms1
- python: 3.6
env: TOX_ENV=py36-WTForms2
addons: addons:
postgresql: "9.4" postgresql: "9.4"
...@@ -24,10 +42,9 @@ before_script: ...@@ -24,10 +42,9 @@ before_script:
- psql -U postgres -c 'CREATE EXTENSION hstore;' flask_admin_test - psql -U postgres -c 'CREATE EXTENSION hstore;' flask_admin_test
install: install:
- pip install "wtforms<$WTFORMS_VERSION.99" - pip install tox
- pip install -r requirements-dev.txt
script: nosetests flask_admin/tests --with-coverage --cover-erase --cover-inclusive script: tox -e $TOX_ENV
after_success: after_success:
- coveralls - coveralls
...@@ -4,10 +4,10 @@ Flask-Admin ...@@ -4,10 +4,10 @@ Flask-Admin
The project was recently moved into its own organization. Please update your The project was recently moved into its own organization. Please update your
references to *git@github.com:flask-admin/flask-admin.git*. references to *git@github.com:flask-admin/flask-admin.git*.
.. image:: https://d322cqt584bo4o.cloudfront.net/flask-admin/localized.png .. image:: https://d322cqt584bo4o.cloudfront.net/flask-admin/localized.svg
:target: https://crowdin.com/project/flask-admin :target: https://crowdin.com/project/flask-admin
.. image:: https://travis-ci.org/flask-admin/flask-admin.png?branch=master .. image:: https://travis-ci.org/flask-admin/flask-admin.svg?branch=master
:target: https://travis-ci.org/flask-admin/flask-admin :target: https://travis-ci.org/flask-admin/flask-admin
Introduction Introduction
...@@ -101,6 +101,8 @@ For all the tests to pass successfully, you'll need Postgres & MongoDB to be run ...@@ -101,6 +101,8 @@ For all the tests to pass successfully, you'll need Postgres & MongoDB to be run
CREATE EXTENSION hstore; CREATE EXTENSION hstore;
\q \q
You can also run the tests on multiple environments using *tox*.
3rd Party Stuff 3rd Party Stuff
--------------- ---------------
......
This diff is collapsed.
Changelog Changelog
========= =========
1.4.2 1.5.0
----- -----
* Fixed CSRF generation logic for multi-process deployments
* Added WTForms >= 3.0 support
* Flask-Admin would not recursively save inline models, allowing arbitrary nesting
* Added configuration properties that allow injection of additional CSS and JS dependencies into templates without overriding them
* SQLAlchemy backend
- Updated hybrid property detection using new SQLAlchemy APIs
- Added support for association proxies
- Added support for remote hybrid properties filters
- Added support for ARRAY column type
* Localization-related fixes
* MongoEngine backend is now properly formats model labels
* Improved Google App Engine support:
- Added TextProperty, KeyProperty and SelectField support
- Added support for form_args, excluded_columns, page_size and after_model_update
* Fixed URL generation with localized named filters
* FileAdmin has Bootstrap 2 support now
* Geoalchemy fixes
- Use Google Places (by default) for place search
* Updated translations
* Bug fixes
1.4.2
-----
* Small bug fix release. Fixes regression that prevented usage of "virtual" columns with a custom formatter. * Small bug fix release. Fixes regression that prevented usage of "virtual" columns with a custom formatter.
1.4.1 1.4.1
......
...@@ -102,7 +102,7 @@ could be as simple as:: ...@@ -102,7 +102,7 @@ could be as simple as::
class MicroBlogModelView(sqla.ModelView): class MicroBlogModelView(sqla.ModelView):
def is_accessible(self): def is_accessible(self):
return login.current_user.is_authenticated() return login.current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs): def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access # redirect to login page if user doesn't have access
......
Flask Flask
Flask-Admin Flask-Admin
pymongo==2.4.1 pymongo==2.4.1
bson
__version__ = '1.4.2' __version__ = '1.5.0'
__author__ = 'Flask-Admin team' __author__ = 'Flask-Admin team'
__email__ = 'serge.koval+github@gmail.com' __email__ = 'serge.koval+github@gmail.com'
from .base import expose, expose_plugview, Admin, BaseView, AdminIndexView from .base import expose, expose_plugview, Admin, BaseView, AdminIndexView # noqa: F401
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# flake8: noqa
""" """
flask_admin._compat flask_admin._compat
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~
...@@ -87,17 +88,4 @@ def with_metaclass(meta, *bases): ...@@ -87,17 +88,4 @@ def with_metaclass(meta, *bases):
try: try:
from collections import OrderedDict from collections import OrderedDict
except ImportError: except ImportError:
# Bare-bones OrderedDict implementation for Python2.6 compatibility from ordereddict import OrderedDict
class OrderedDict(dict):
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
self.ordered_keys = []
def __setitem__(self, key, value):
self.ordered_keys.append(key)
dict.__setitem__(self, key, value)
def __iter__(self):
return (k for k in self.ordered_keys)
def iteritems(self):
return ((k, self[k]) for k in self.ordered_keys)
def items(self):
return list(self.iteritems())
...@@ -3,7 +3,7 @@ from flask import request, redirect ...@@ -3,7 +3,7 @@ from flask import request, redirect
from flask_admin import tools from flask_admin import tools
from flask_admin._compat import text_type from flask_admin._compat import text_type
from flask_admin.helpers import get_redirect_target from flask_admin.helpers import get_redirect_target, flash_errors
def action(name, text, confirmation=None): def action(name, text, confirmation=None):
...@@ -104,8 +104,12 @@ class ActionsMixin(object): ...@@ -104,8 +104,12 @@ class ActionsMixin(object):
If not provided, will return user to the return url in the form If not provided, will return user to the return url in the form
or the list view. or the list view.
""" """
action = request.form.get('action') form = self.action_form()
if self.validate_form(form):
# using getlist instead of FieldList for backward compatibility
ids = request.form.getlist('rowid') ids = request.form.getlist('rowid')
action = form.action.data
handler = self._actions_data.get(action) handler = self._actions_data.get(action)
...@@ -114,6 +118,8 @@ class ActionsMixin(object): ...@@ -114,6 +118,8 @@ class ActionsMixin(object):
if response is not None: if response is not None:
return response return response
else:
flash_errors(form, message='Failed to perform action. %(error)s')
if return_view: if return_view:
url = self.get_url('.' + return_view) url = self.get_url('.' + return_view)
......
...@@ -9,7 +9,7 @@ from flask_admin._compat import with_metaclass, as_unicode ...@@ -9,7 +9,7 @@ from flask_admin._compat import with_metaclass, as_unicode
from flask_admin import helpers as h from flask_admin import helpers as h
# For compatibility reasons import MenuLink # For compatibility reasons import MenuLink
from flask_admin.menu import MenuCategory, MenuView, MenuLink from flask_admin.menu import MenuCategory, MenuView, MenuLink # noqa: F401
def expose(url='/', methods=('GET',)): def expose(url='/', methods=('GET',)):
...@@ -664,13 +664,17 @@ class Admin(object): ...@@ -664,13 +664,17 @@ class Admin(object):
self._init_extension() self._init_extension()
# Register Index view # Register Index view
self._set_admin_index_view(index_view=index_view, endpoint=endpoint, url=url) if index_view is not None:
self._set_admin_index_view(
index_view=index_view,
endpoint=endpoint,
url=url
)
# Register views # Register views
for view in self._views: for view in self._views:
app.register_blueprint(view.create_blueprint(self)) app.register_blueprint(view.create_blueprint(self))
def _init_extension(self): def _init_extension(self):
if not hasattr(self.app, 'extensions'): if not hasattr(self.app, 'extensions'):
self.app.extensions = dict() self.app.extensions = dict()
......
# flake8: noqa
try: try:
import wtforms_appengine import wtforms_appengine
except ImportError: except ImportError:
......
...@@ -3,6 +3,7 @@ from google.appengine.ext import ndb ...@@ -3,6 +3,7 @@ from google.appengine.ext import ndb
import decimal import decimal
class GeoPtPropertyField(StringField): class GeoPtPropertyField(StringField):
def process_formdata(self, valuelist): def process_formdata(self, valuelist):
if valuelist: if valuelist:
......
...@@ -2,6 +2,7 @@ from wtforms_appengine.ndb import ModelConverter ...@@ -2,6 +2,7 @@ from wtforms_appengine.ndb import ModelConverter
from .fields import GeoPtPropertyField from .fields import GeoPtPropertyField
from flask_admin.model.form import converts from flask_admin.model.form import converts
class AdminModelConverter(ModelConverter): class AdminModelConverter(ModelConverter):
@converts('GeoPt') @converts('GeoPt')
def convert_GeoPtProperty(self, model, prop, kwargs): def convert_GeoPtProperty(self, model, prop, kwargs):
......
...@@ -11,6 +11,7 @@ from flask_wtf import Form ...@@ -11,6 +11,7 @@ from flask_wtf import Form
from flask_admin.model.form import create_editable_list_form from flask_admin.model.form import create_editable_list_form
from .form import AdminModelConverter from .form import AdminModelConverter
class NdbModelView(BaseModelView): class NdbModelView(BaseModelView):
""" """
AppEngine NDB model scaffolding. AppEngine NDB model scaffolding.
...@@ -32,7 +33,7 @@ class NdbModelView(BaseModelView): ...@@ -32,7 +33,7 @@ class NdbModelView(BaseModelView):
pass pass
def scaffold_filters(self): def scaffold_filters(self):
#TODO: implement # TODO: implement
pass pass
form_args = None form_args = None
...@@ -75,7 +76,7 @@ class NdbModelView(BaseModelView): ...@@ -75,7 +76,7 @@ class NdbModelView(BaseModelView):
def get_list(self, page, sort_field, sort_desc, search, filters, def get_list(self, page, sort_field, sort_desc, search, filters,
page_size=None): 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()
...@@ -88,7 +89,7 @@ class NdbModelView(BaseModelView): ...@@ -88,7 +89,7 @@ class NdbModelView(BaseModelView):
if not page_size: if not page_size:
page_size = self.page_size page_size = self.page_size
results = q.fetch(page_size, offset=page*page_size) results = q.fetch(page_size, offset=page * page_size)
return q.count(), results return q.count(), results
...@@ -102,7 +103,7 @@ class NdbModelView(BaseModelView): ...@@ -102,7 +103,7 @@ class NdbModelView(BaseModelView):
model.put() model.put()
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
...@@ -117,7 +118,7 @@ class NdbModelView(BaseModelView): ...@@ -117,7 +118,7 @@ class NdbModelView(BaseModelView):
model.put() model.put()
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
...@@ -131,7 +132,7 @@ class NdbModelView(BaseModelView): ...@@ -131,7 +132,7 @@ class NdbModelView(BaseModelView):
model.key.delete() model.key.delete()
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',
# error=ex), # error=ex),
# 'error') # 'error')
logging.exception('Failed to delete record.') logging.exception('Failed to delete record.')
...@@ -155,7 +156,8 @@ class DbModelView(BaseModelView): ...@@ -155,7 +156,8 @@ class DbModelView(BaseModelView):
def scaffold_sortable_columns(self): def scaffold_sortable_columns(self):
# We use getattr() because ReferenceProperty does not specify a 'indexed' field # We use getattr() because ReferenceProperty does not specify a 'indexed' field
return [k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, db.Property) and getattr(v, 'indexed', None)] return [k for (k, v) in self.model.__dict__.iteritems()
if isinstance(v, db.Property) and getattr(v, 'indexed', None)]
def init_search(self): def init_search(self):
return None return None
...@@ -164,14 +166,14 @@ class DbModelView(BaseModelView): ...@@ -164,14 +166,14 @@ class DbModelView(BaseModelView):
pass pass
def scaffold_filters(self): def scaffold_filters(self):
#TODO: implement # TODO: implement
pass pass
def scaffold_form(self): def scaffold_form(self):
return wt_db.model_form(self.model()) return wt_db.model_form(self.model())
def get_list(self, page, sort_field, sort_desc, search, filters): def get_list(self, page, sort_field, sort_desc, search, filters):
#TODO: implement filters (don't think search can work here) # TODO: implement filters (don't think search can work here)
q = self.model.all() q = self.model.all()
...@@ -180,7 +182,7 @@ class DbModelView(BaseModelView): ...@@ -180,7 +182,7 @@ class DbModelView(BaseModelView):
sort_field = "-" + sort_field sort_field = "-" + sort_field
q.order(sort_field) q.order(sort_field)
results = q.fetch(self.page_size, offset=page*self.page_size) results = q.fetch(self.page_size, offset=page * self.page_size)
return q.count(), results return q.count(), results
def get_one(self, encoded_key): def get_one(self, encoded_key):
...@@ -194,7 +196,7 @@ class DbModelView(BaseModelView): ...@@ -194,7 +196,7 @@ class DbModelView(BaseModelView):
return model 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
...@@ -206,7 +208,7 @@ class DbModelView(BaseModelView): ...@@ -206,7 +208,7 @@ class DbModelView(BaseModelView):
return True 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
...@@ -217,12 +219,13 @@ class DbModelView(BaseModelView): ...@@ -217,12 +219,13 @@ class DbModelView(BaseModelView):
return True 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',
# error=ex), # error=ex),
# 'error') # 'error')
logging.exception('Failed to delete record.') logging.exception('Failed to delete record.')
return False return False
def ModelView(model): def ModelView(model):
if issubclass(model, ndb.Model): if issubclass(model, ndb.Model):
return NdbModelView(model) return NdbModelView(model)
......
...@@ -413,6 +413,19 @@ class BaseFileAdmin(BaseView, ActionsMixin): ...@@ -413,6 +413,19 @@ class BaseFileAdmin(BaseView, ActionsMixin):
return DeleteForm return DeleteForm
def get_action_form(self):
"""
Create form class for model action.
Override to implement customized behavior.
"""
class ActionForm(self.form_base_class):
action = fields.HiddenField()
url = fields.HiddenField()
# rowid is retrieved using getlist, for backward compatibility
return ActionForm
def upload_form(self): def upload_form(self):
""" """
Instantiate file upload form and return it. Instantiate file upload form and return it.
...@@ -471,6 +484,18 @@ class BaseFileAdmin(BaseView, ActionsMixin): ...@@ -471,6 +484,18 @@ class BaseFileAdmin(BaseView, ActionsMixin):
else: else:
return delete_form_class() return delete_form_class()
def action_form(self):
"""
Instantiate action form and return it.
Override to implement custom behavior.
"""
action_form_class = self.get_action_form()
if request.form:
return action_form_class(request.form)
else:
return action_form_class()
def is_file_allowed(self, filename): def is_file_allowed(self, filename):
""" """
Verify if file can be uploaded. Verify if file can be uploaded.
...@@ -802,7 +827,7 @@ class BaseFileAdmin(BaseView, ActionsMixin): ...@@ -802,7 +827,7 @@ class BaseFileAdmin(BaseView, ActionsMixin):
# Sort by type # Sort by type
items.sort(key=itemgetter(2), reverse=True) items.sort(key=itemgetter(2), reverse=True)
# Sort by modified date # Sort by modified date
items.sort(key=lambda values: (values[0], values[1], values[2], values[3], datetime.fromtimestamp(values[4])), reverse=True) items.sort(key=lambda x: (x[0], x[1], x[2], x[3], datetime.fromtimestamp(x[4])), reverse=True)
else: else:
column_index = self.possible_columns.index(sort_column) column_index = self.possible_columns.index(sort_column)
items.sort(key=itemgetter(column_index), reverse=sort_desc) items.sort(key=itemgetter(column_index), reverse=sort_desc)
...@@ -812,6 +837,10 @@ class BaseFileAdmin(BaseView, ActionsMixin): ...@@ -812,6 +837,10 @@ class BaseFileAdmin(BaseView, ActionsMixin):
# Actions # Actions
actions, actions_confirmation = self.get_actions_list() actions, actions_confirmation = self.get_actions_list()
if actions:
action_form = self.action_form()
else:
action_form = None
def sort_url(column, invert=False): def sort_url(column, invert=False):
desc = None desc = None
...@@ -829,6 +858,7 @@ class BaseFileAdmin(BaseView, ActionsMixin): ...@@ -829,6 +858,7 @@ class BaseFileAdmin(BaseView, ActionsMixin):
items=items, items=items,
actions=actions, actions=actions,
actions_confirmation=actions_confirmation, actions_confirmation=actions_confirmation,
action_form=action_form,
delete_form=delete_form, delete_form=delete_form,
sort_column=sort_column, sort_column=sort_column,
sort_desc=sort_desc, sort_desc=sort_desc,
......
...@@ -55,10 +55,11 @@ class S3Storage(object): ...@@ -55,10 +55,11 @@ class S3Storage(object):
raise ValueError('Could not import boto. You can install boto by ' raise ValueError('Could not import boto. You can install boto by '
'using pip install boto') 'using pip install boto')
connection = s3.connect_to_region(region, connection = s3.connect_to_region(
region,
aws_access_key_id=aws_access_key_id, aws_access_key_id=aws_access_key_id,
aws_secret_access_key= aws_secret_access_key=aws_secret_access_key,
aws_secret_access_key) )
self.bucket = connection.get_bucket(bucket_name) self.bucket = connection.get_bucket(bucket_name)
self.separator = '/' self.separator = '/'
......
# flake8: noqa
try: try:
import geoalchemy2 import geoalchemy2
import shapely import shapely
......
...@@ -38,9 +38,9 @@ class LeafletWidget(TextArea): ...@@ -38,9 +38,9 @@ class LeafletWidget(TextArea):
kwargs.setdefault('data-geometry-type', gtype) kwargs.setdefault('data-geometry-type', gtype)
# set optional values from constructor # set optional values from constructor
if not "data-width" in kwargs: if "data-width" not in kwargs:
kwargs["data-width"] = self.width kwargs["data-width"] = self.width
if not "data-height" in kwargs: if "data-height" not in kwargs:
kwargs["data-height"] = self.height kwargs["data-height"] = self.height
if self.center: if self.center:
kwargs["data-lat"] = lat(self.center) kwargs["data-lat"] = lat(self.center)
......
# flake8: noqa
try: try:
import flask_mongoengine import flask_mongoengine
except ImportError: except ImportError:
......
...@@ -7,7 +7,6 @@ from flask_mongoengine.wtf import orm, fields as mongo_fields ...@@ -7,7 +7,6 @@ from flask_mongoengine.wtf import orm, fields as mongo_fields
from flask_admin import form from flask_admin import form
from flask_admin.model.form import FieldPlaceholder from flask_admin.model.form import FieldPlaceholder
from flask_admin.model.fields import InlineFieldList, AjaxSelectField, AjaxSelectMultipleField from flask_admin.model.fields import InlineFieldList, AjaxSelectField, AjaxSelectMultipleField
from flask_admin.model.widgets import InlineFormWidget
from flask_admin._compat import iteritems from flask_admin._compat import iteritems
from .fields import ModelFormField, MongoFileField, MongoImageField from .fields import ModelFormField, MongoFileField, MongoImageField
...@@ -60,7 +59,7 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -60,7 +59,7 @@ class CustomModelConverter(orm.ModelConverter):
return form.recreate_field(field.field) return form.recreate_field(field.field)
kwargs = { kwargs = {
'label': getattr(field, 'verbose_name', field.name), 'label': getattr(field, 'verbose_name', None),
'description': getattr(field, 'help_text', ''), 'description': getattr(field, 'help_text', ''),
'validators': [], 'validators': [],
'filters': [], 'filters': [],
......
...@@ -18,6 +18,7 @@ def convert_subdocuments(values): ...@@ -18,6 +18,7 @@ def convert_subdocuments(values):
elif isinstance(p, EmbeddedForm): elif isinstance(p, EmbeddedForm):
result[name] = p result[name] = p
else: else:
raise ValueError('Invalid subdocument type: expecting dict or instance of flask_admin.contrib.mongoengine.EmbeddedForm, got %s' % type(p)) raise ValueError('Invalid subdocument type: expecting dict or '
'instance of flask_admin.contrib.mongoengine.EmbeddedForm, got %s' % type(p))
return result return result
...@@ -24,5 +24,5 @@ def parse_like_term(term): ...@@ -24,5 +24,5 @@ def parse_like_term(term):
oper = 'contains' oper = 'contains'
# add case insensitive flag # add case insensitive flag
if case_insensitive: if case_insensitive:
oper = 'i'+oper oper = 'i' + oper
return oper, term return oper, term
...@@ -626,7 +626,6 @@ class ModelView(BaseModelView): ...@@ -626,7 +626,6 @@ class ModelView(BaseModelView):
return True return True
# FileField access API # FileField access API
@expose('/api/file/') @expose('/api/file/')
def api_file_view(self): def api_file_view(self):
...@@ -645,9 +644,7 @@ class ModelView(BaseModelView): ...@@ -645,9 +644,7 @@ class ModelView(BaseModelView):
return Response(data.read(), return Response(data.read(),
content_type=data.content_type, content_type=data.content_type,
headers={ headers={'Content-Length': data.length})
'Content-Length': data.length
})
# Default model actions # Default model actions
def is_action_allowed(self, name): def is_action_allowed(self, name):
......
# flake8: noqa
try: try:
import peewee import peewee
import wtfpeewee import wtfpeewee
......
...@@ -319,7 +319,7 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -319,7 +319,7 @@ class FilterConverter(filters.BaseFilterConverter):
def conv_bool(self, column, name): def conv_bool(self, column, name):
return [f(column, name) for f in self.bool_filters] return [f(column, name) for f in self.bool_filters]
@filters.convert('IntegerField', 'BigIntegerField') @filters.convert('IntegerField', 'BigIntegerField', 'PrimaryKeyField')
def conv_int(self, column, name): def conv_int(self, column, name):
return [f(column, name) for f in self.int_filters] return [f(column, name) for f in self.int_filters]
......
...@@ -12,6 +12,11 @@ from flask_admin.model.fields import InlineModelFormField, InlineFieldList, Ajax ...@@ -12,6 +12,11 @@ from flask_admin.model.fields import InlineModelFormField, InlineFieldList, Ajax
from .tools import get_primary_key, get_meta_fields from .tools import get_primary_key, get_meta_fields
from .ajax import create_ajax_loader from .ajax import create_ajax_loader
try:
from playhouse.postgres_ext import JSONField, BinaryJSONField
pg_ext = True
except:
pg_ext = False
class InlineModelFormList(InlineFieldList): class InlineModelFormList(InlineFieldList):
...@@ -36,15 +41,22 @@ class InlineModelFormList(InlineFieldList): ...@@ -36,15 +41,22 @@ class InlineModelFormList(InlineFieldList):
def display_row_controls(self, field): def display_row_controls(self, field):
return field.get_pk() is not None return field.get_pk() is not None
# *** bryhoyt removed def process() entirely, because I believe it was buggy """ bryhoyt removed def process() entirely, because I believe it was buggy
# (but worked because another part of the code had a complimentary bug) (but worked because another part of the code had a complimentary bug)
# and I'm not sure why it was necessary anyway. and I'm not sure why it was necessary anyway.
# If we want it back in, we need to fix the following bogus query:
# self.model.select().where(attr == data).execute() # `data` is not an ID, and only happened to be so because we patched it in in .contribute() below If we want it back in, we need to fix the following bogus query:
# self.model.select().where(attr == data).execute()
# For reference:
# .process() introduced in https://github.com/flask-admin/flask-admin/commit/2845e4b28cb40b25e2bf544b327f6202dc7e5709 `data` is not an ID, and only happened to be so because we patched it
# Fixed, brokenly I think, in https://github.com/flask-admin/flask-admin/commit/4383eef3ce7eb01878f086928f8773adb9de79f8#diff-f87e7cd76fb9bc48c8681b24f238fb13R30 in in .contribute() below
For reference, .process() introduced in:
https://github.com/flask-admin/flask-admin/commit/2845e4b28cb40b25e2bf544b327f6202dc7e5709
Fixed, brokenly I think, in:
https://github.com/flask-admin/flask-admin/commit/4383eef3ce7eb01878f086928f8773adb9de79f8#diff-f87e7cd76fb9bc48c8681b24f238fb13R30
"""
def populate_obj(self, obj, name): def populate_obj(self, obj, name):
pass pass
...@@ -99,6 +111,10 @@ class CustomModelConverter(ModelConverter): ...@@ -99,6 +111,10 @@ class CustomModelConverter(ModelConverter):
self.converters[DateField] = self.handle_date self.converters[DateField] = self.handle_date
self.converters[TimeField] = self.handle_time self.converters[TimeField] = self.handle_time
if pg_ext:
self.converters[JSONField] = self.handle_json
self.converters[BinaryJSONField] = self.handle_json
self.overrides = getattr(self.view, 'form_overrides', None) or {} self.overrides = getattr(self.view, 'form_overrides', None) or {}
def handle_foreign_key(self, model, field, **kwargs): def handle_foreign_key(self, model, field, **kwargs):
...@@ -127,6 +143,9 @@ class CustomModelConverter(ModelConverter): ...@@ -127,6 +143,9 @@ class CustomModelConverter(ModelConverter):
def handle_time(self, model, field, **kwargs): def handle_time(self, model, field, **kwargs):
return field.name, form.TimeField(**kwargs) return field.name, form.TimeField(**kwargs)
def handle_json(self, model, field, **kwargs):
return field.name, form.JSONField(**kwargs)
def get_form(model, converter, def get_form(model, converter,
base_class=form.BaseForm, base_class=form.BaseForm,
...@@ -246,7 +265,6 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -246,7 +265,6 @@ class InlineModelConverter(InlineModelConverterBase):
allow_pk=True, allow_pk=True,
converter=converter) converter=converter)
prop_name = reverse_field.related_name prop_name = reverse_field.related_name
label = self.get_label(info, prop_name) label = self.get_label(info, prop_name)
......
def setup(): def setup():
import warnings import warnings
warnings.warn('Flask-Admin peewee integration module was renamed as flask_admin.contrib.peewee, please use it instead.') warnings.warn('Flask-Admin peewee integration module was renamed as '
'flask_admin.contrib.peewee, please use it instead.')
from flask_admin._backwards import import_redirect from flask_admin._backwards import import_redirect
import_redirect(__name__, 'flask_admin.contrib.peewee') import_redirect(__name__, 'flask_admin.contrib.peewee')
setup() setup()
del setup del setup
from ..peewee.view import ModelView from ..peewee.view import ModelView # noqa: F401
# flake8: noqa
try: try:
import pymongo import pymongo
except ImportError: except ImportError:
......
import re import re
def parse_like_term(term): def parse_like_term(term):
""" """
Parse search term into (operation, term) tuple Parse search term into (operation, term) tuple
......
...@@ -82,8 +82,10 @@ class RedisCli(BaseView): ...@@ -82,8 +82,10 @@ class RedisCli(BaseView):
self._contribute_commands() self._contribute_commands()
if self.shlex_check and VER < (2, 7, 3): if self.shlex_check and VER < (2, 7, 3):
warnings.warn('Warning: rediscli uses shlex library and it does not work with unicode until Python 2.7.3. ' + warnings.warn('Warning: rediscli uses shlex library and it does '
'To remove this warning, upgrade to Python 2.7.3 or suppress it by setting shlex_check attribute ' + 'not work with unicode until Python 2.7.3. To '
'remove this warning, upgrade to Python 2.7.3 or '
'suppress it by setting shlex_check attribute '
'to False.') 'to False.')
def _inspect_commands(self): def _inspect_commands(self):
......
# flake8: noqa
from .view import ModelView from .view import ModelView
...@@ -86,9 +86,9 @@ class FilterSmaller(BaseSQLAFilter): ...@@ -86,9 +86,9 @@ class FilterSmaller(BaseSQLAFilter):
class FilterEmpty(BaseSQLAFilter, filters.BaseBooleanFilter): class FilterEmpty(BaseSQLAFilter, filters.BaseBooleanFilter):
def apply(self, query, value, alias=None): def apply(self, query, value, alias=None):
if value == '1': if value == '1':
return query.filter(self.get_column(alias) == None) return query.filter(self.get_column(alias) == None) # noqa: E711
else: else:
return query.filter(self.get_column(alias) != None) return query.filter(self.get_column(alias) != None) # noqa: E711
def operation(self): def operation(self):
return lazy_gettext('empty') return lazy_gettext('empty')
...@@ -112,7 +112,7 @@ class FilterNotInList(FilterInList): ...@@ -112,7 +112,7 @@ class FilterNotInList(FilterInList):
def apply(self, query, value, alias=None): def apply(self, query, value, alias=None):
# NOT IN can exclude NULL values, so "or_ == None" needed to be added # NOT IN can exclude NULL values, so "or_ == None" needed to be added
column = self.get_column(alias) column = self.get_column(alias)
return query.filter(or_(~column.in_(value), column == None)) return query.filter(or_(~column.in_(value), column == None)) # noqa: E711
def operation(self): def operation(self):
return lazy_gettext('not in list') return lazy_gettext('not in list')
...@@ -323,7 +323,7 @@ class EnumFilterInList(FilterInList): ...@@ -323,7 +323,7 @@ class EnumFilterInList(FilterInList):
def clean(self, value): def clean(self, value):
values = super(EnumFilterInList, self).clean(value) values = super(EnumFilterInList, self).clean(value)
if self.enum_class is not None: if self.enum_class is not None:
values = [self.enum_class(value) for value in values] values = [self.enum_class(val) for val in values]
return values return values
...@@ -335,7 +335,7 @@ class EnumFilterNotInList(FilterNotInList): ...@@ -335,7 +335,7 @@ class EnumFilterNotInList(FilterNotInList):
def clean(self, value): def clean(self, value):
values = super(EnumFilterNotInList, self).clean(value) values = super(EnumFilterNotInList, self).clean(value)
if self.enum_class is not None: if self.enum_class is not None:
values = [self.enum_class(value) for value in values] values = [self.enum_class(val) for val in values]
return values return values
......
...@@ -201,10 +201,10 @@ class AdminModelConverter(ModelConverterBase): ...@@ -201,10 +201,10 @@ class AdminModelConverter(ModelConverterBase):
optional_types = getattr(self.view, 'form_optional_types', (Boolean,)) optional_types = getattr(self.view, 'form_optional_types', (Boolean,))
if ( if (
not column.nullable not column.nullable and
and not isinstance(column.type, optional_types) not isinstance(column.type, optional_types) and
and not column.default not column.default and
and not column.server_default not column.server_default
): ):
kwargs['validators'].append(validators.InputRequired()) kwargs['validators'].append(validators.InputRequired())
...@@ -222,7 +222,7 @@ class AdminModelConverter(ModelConverterBase): ...@@ -222,7 +222,7 @@ class AdminModelConverter(ModelConverterBase):
if value is not None: if value is not None:
if getattr(default, 'is_callable', False): if getattr(default, 'is_callable', False):
value = lambda: default.arg(None) value = lambda: default.arg(None) # noqa: E731
else: else:
if not getattr(default, 'is_scalar', True): if not getattr(default, 'is_scalar', True):
value = None value = None
...@@ -243,7 +243,7 @@ class AdminModelConverter(ModelConverterBase): ...@@ -243,7 +243,7 @@ class AdminModelConverter(ModelConverterBase):
form_choices = getattr(self.view, 'form_choices', None) form_choices = getattr(self.view, 'form_choices', None)
if mapper.class_ == self.view.model and form_choices: if mapper.class_ == self.view.model and form_choices:
choices = form_choices.get(column.key) choices = form_choices.get(prop.key)
if choices: if choices:
return form.Select2Field( return form.Select2Field(
choices=choices, choices=choices,
...@@ -547,8 +547,6 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -547,8 +547,6 @@ class InlineModelConverter(InlineModelConverterBase):
:return: :return:
A tuple of forward property key and reverse property key A tuple of forward property key and reverse property key
""" """
mapper = model._sa_class_manager.mapper mapper = model._sa_class_manager.mapper
# Find property from target model to current model # Find property from target model to current model
......
...@@ -9,7 +9,7 @@ from sqlalchemy.exc import DBAPIError ...@@ -9,7 +9,7 @@ from sqlalchemy.exc import DBAPIError
from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.orm.attributes import InstrumentedAttribute
from flask_admin._compat import filter_list, string_types from flask_admin._compat import filter_list, string_types
from flask_admin.tools import iterencode, iterdecode, escape from flask_admin.tools import iterencode, iterdecode, escape # noqa: F401
def parse_like_term(term): def parse_like_term(term):
...@@ -79,9 +79,9 @@ def tuple_operator_in(model_pk, ids): ...@@ -79,9 +79,9 @@ def tuple_operator_in(model_pk, ids):
for id in ids: for id in ids:
k = [] k = []
for i in range(len(model_pk)): for i in range(len(model_pk)):
k.append(eq(model_pk[i],id[i])) k.append(eq(model_pk[i], id[i]))
l.append(and_(*k)) l.append(and_(*k))
if len(l)>=1: if len(l) >= 1:
return or_(*l) return or_(*l)
else: else:
return None return None
...@@ -196,7 +196,7 @@ def is_hybrid_property(model, attr_name): ...@@ -196,7 +196,7 @@ def is_hybrid_property(model, attr_name):
if isinstance(attr_name, string_types): if isinstance(attr_name, string_types):
names = attr_name.split('.') names = attr_name.split('.')
last_model = model last_model = model
for i in range(len(names)-1): for i in range(len(names) - 1):
attr = getattr(last_model, names[i]) attr = getattr(last_model, names[i])
if is_association_proxy(attr): if is_association_proxy(attr):
attr = attr.remote_attr attr = attr.remote_attr
......
...@@ -2,7 +2,6 @@ import logging ...@@ -2,7 +2,6 @@ import logging
import warnings import warnings
import inspect import inspect
from speaklater import is_lazy_string, make_lazy_string
from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm import joinedload, aliased from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.expression import desc from sqlalchemy.sql.expression import desc
...@@ -631,7 +630,7 @@ class ModelView(BaseModelView): ...@@ -631,7 +630,7 @@ class ModelView(BaseModelView):
# If filter related to relation column (represented by # If filter related to relation column (represented by
# relation_name.target_column) we collect here relation name # relation_name.target_column) we collect here relation name
joined_column_name = None joined_column_name = None
if '.' in name: if isinstance(name, string_types) and '.' in name:
joined_column_name = name.split('.')[0] joined_column_name = name.split('.')[0]
# Join not needed for hybrid properties # Join not needed for hybrid properties
...@@ -652,14 +651,11 @@ class ModelView(BaseModelView): ...@@ -652,14 +651,11 @@ class ModelView(BaseModelView):
if not isinstance(name, string_types): if not isinstance(name, string_types):
visible_name = self.get_column_name(name.property.key) visible_name = self.get_column_name(name.property.key)
else: else:
column_name = self.get_column_name(name) if self.column_labels and name in self.column_labels:
visible_name = self.column_labels[name]
def prettify():
return column_name.replace('.', ' / ')
if is_lazy_string(column_name):
visible_name = make_lazy_string(prettify)
else: else:
visible_name = prettify() visible_name = self.get_column_name(name)
visible_name = visible_name.replace('.', ' / ')
type_name = type(column.type).__name__ type_name = type(column.type).__name__
...@@ -944,7 +940,8 @@ class ModelView(BaseModelView): ...@@ -944,7 +940,8 @@ class ModelView(BaseModelView):
spec = inspect.getargspec(flt.apply) spec = inspect.getargspec(flt.apply)
if len(spec.args) == 3: if len(spec.args) == 3:
warnings.warn('Please update your custom filter %s to include additional `alias` parameter.' % repr(flt)) warnings.warn('Please update your custom filter %s to '
'include additional `alias` parameter.' % repr(flt))
else: else:
raise raise
......
def setup(): def setup():
import warnings import warnings
warnings.warn('Flask-Admin sqlalchemy integration module was renamed as flask_admin.contrib.sqla, please use it instead.') warnings.warn('Flask-Admin sqlalchemy integration module was renamed as '
'flask_admin.contrib.sqla, please use it instead.')
from flask_admin._backwards import import_redirect from flask_admin._backwards import import_redirect
import_redirect(__name__, 'flask_admin.contrib.sqla') import_redirect(__name__, 'flask_admin.contrib.sqla')
setup() setup()
del setup del setup
from ..sqla.view import ModelView from ..sqla.view import ModelView # noqa: F401
...@@ -2,9 +2,9 @@ from wtforms import form, __version__ as wtforms_version ...@@ -2,9 +2,9 @@ from wtforms import form, __version__ as wtforms_version
from wtforms.fields.core import UnboundField from wtforms.fields.core import UnboundField
from flask_admin.babel import Translations from flask_admin.babel import Translations
from .fields import * from .fields import * # noqa: F403,F401
from .widgets import * from .widgets import * # noqa: F403,F401
from .upload import * from .upload import * # noqa: F403,F401
class BaseForm(form.Form): class BaseForm(form.Form):
......
...@@ -9,7 +9,8 @@ from flask_admin._compat import text_type, as_unicode ...@@ -9,7 +9,8 @@ from flask_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 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',
...@@ -21,6 +22,7 @@ class DateTimeField(fields.DateTimeField): ...@@ -21,6 +22,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, **kwargs): def __init__(self, label=None, validators=None, format=None, **kwargs):
""" """
Constructor Constructor
...@@ -166,6 +168,7 @@ class Select2TagsField(fields.StringField): ...@@ -166,6 +168,7 @@ class Select2TagsField(fields.StringField):
super(Select2TagsField, self).__init__(label, validators, **kwargs) super(Select2TagsField, self).__init__(label, validators, **kwargs)
def process_formdata(self, valuelist): def process_formdata(self, valuelist):
if valuelist:
if self.save_as_list: if self.save_as_list:
self.data = [self.coerce(v.strip()) for v in valuelist[0].split(',') if v.strip()] self.data = [self.coerce(v.strip()) for v in valuelist[0].split(',') if v.strip()]
else: else:
......
...@@ -83,6 +83,7 @@ class ImageUploadInput(object): ...@@ -83,6 +83,7 @@ class ImageUploadInput(object):
data_template = ('<div class="image-thumbnail">' data_template = ('<div class="image-thumbnail">'
' <img %(image)s>' ' <img %(image)s>'
' <input type="checkbox" name="%(marker)s">Delete</input>' ' <input type="checkbox" name="%(marker)s">Delete</input>'
' <input %(text)s>'
'</div>' '</div>'
'<input %(file)s>') '<input %(file)s>')
...@@ -91,6 +92,9 @@ class ImageUploadInput(object): ...@@ -91,6 +92,9 @@ class ImageUploadInput(object):
kwargs.setdefault('name', field.name) kwargs.setdefault('name', field.name)
args = { args = {
'text': html_params(type='hidden',
value=field.data,
name=field.name),
'file': html_params(type='file', 'file': html_params(type='file',
**kwargs), **kwargs),
'marker': '_%s-delete' % field.name 'marker': '_%s-delete' % field.name
...@@ -196,9 +200,7 @@ class FileUploadField(fields.StringField): ...@@ -196,9 +200,7 @@ class FileUploadField(fields.StringField):
map(lambda x: x.lower(), self.allowed_extensions)) map(lambda x: x.lower(), self.allowed_extensions))
def _is_uploaded_file(self, data): def _is_uploaded_file(self, data):
return (data return (data and isinstance(data, FileStorage) and data.filename)
and isinstance(data, FileStorage)
and data.filename)
def pre_validate(self, form): def pre_validate(self, form):
if self._is_uploaded_file(self.data) and not self.is_file_allowed(self.data.filename): if self._is_uploaded_file(self.data) and not self.is_file_allowed(self.data.filename):
...@@ -275,7 +277,7 @@ class FileUploadField(fields.StringField): ...@@ -275,7 +277,7 @@ class FileUploadField(fields.StringField):
if not op.exists(op.dirname(path)): if not op.exists(op.dirname(path)):
os.makedirs(os.path.dirname(path), self.permission | 0o111) os.makedirs(os.path.dirname(path), self.permission | 0o111)
if self._allow_overwrite == False and os.path.exists(path): if (self._allow_overwrite is False) and os.path.exists(path):
raise ValueError(gettext('File "%s" already exists.' % path)) raise ValueError(gettext('File "%s" already exists.' % path))
data.save(path) data.save(path)
......
...@@ -33,12 +33,10 @@ class Select2TagsWidget(widgets.TextInput): ...@@ -33,12 +33,10 @@ class Select2TagsWidget(widgets.TextInput):
You must include select2.js, form-x.x.x.js and select2 stylesheet for it to work. You must include select2.js, form-x.x.x.js and select2 stylesheet for it to work.
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', u'select2') kwargs.setdefault('data-role', u'select2-tags')
kwargs.setdefault('data-tags', u'1')
return super(Select2TagsWidget, self).__call__(field, **kwargs) return super(Select2TagsWidget, self).__call__(field, **kwargs)
class DatePickerWidget(widgets.TextInput): class DatePickerWidget(widgets.TextInput):
""" """
Date picker widget. Date picker widget.
......
...@@ -101,6 +101,7 @@ def flash_errors(form, message): ...@@ -101,6 +101,7 @@ def flash_errors(form, message):
errors = form[field_name].label.text + u": " + u", ".join(errors) errors = form[field_name].label.text + u": " + u", ".join(errors)
flash(gettext(message, error=str(errors)), 'error') flash(gettext(message, error=str(errors)), 'error')
@contextfunction @contextfunction
def resolve_ctx(context): def resolve_ctx(context):
""" """
......
# flake8: noqa
from .base import BaseModelView from .base import BaseModelView
from .form import InlineFormAdmin from .form import InlineFormAdmin
from flask_admin.actions import action from flask_admin.actions import action
...@@ -42,7 +42,8 @@ class ViewArgs(object): ...@@ -42,7 +42,8 @@ class ViewArgs(object):
""" """
List view arguments. List view arguments.
""" """
def __init__(self, page=None, page_size=None, sort=None, sort_desc=None, search=None, filters=None, extra_args=None): def __init__(self, page=None, page_size=None, sort=None, sort_desc=None,
search=None, filters=None, extra_args=None):
self.page = page self.page = page
self.page_size = page_size self.page_size = page_size
self.sort = sort self.sort = sort
...@@ -599,7 +600,10 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -599,7 +600,10 @@ class BaseModelView(BaseView, ActionsMixin):
start=dict(format='%Y-%m-%d %I:%M %p') # changes how the input is parsed by strptime (12 hour time) start=dict(format='%Y-%m-%d %I:%M %p') # changes how the input is parsed by strptime (12 hour time)
) )
form_widget_args = dict( 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 start={
'data-date-format': u'yyyy-mm-dd HH:ii P',
'data-show-meridian': 'True'
} # changes how the DateTimeField displays the time
) )
""" """
...@@ -795,6 +799,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -795,6 +799,7 @@ class BaseModelView(BaseView, ActionsMixin):
self._create_form_class = self.get_create_form() self._create_form_class = self.get_create_form()
self._edit_form_class = self.get_edit_form() self._edit_form_class = self.get_edit_form()
self._delete_form_class = self.get_delete_form() self._delete_form_class = self.get_delete_form()
self._action_form_class = self.get_action_form()
# List View In-Line Editing # List View In-Line Editing
if self.column_editable_list: if self.column_editable_list:
...@@ -855,6 +860,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -855,6 +860,7 @@ class BaseModelView(BaseView, ActionsMixin):
self._sortable_columns = self.get_sortable_columns() self._sortable_columns = self.get_sortable_columns()
# Details view # Details view
if self.can_view_details:
self._details_columns = self.get_details_columns() self._details_columns = self.get_details_columns()
# Export view # Export view
...@@ -937,7 +943,8 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -937,7 +943,8 @@ class BaseModelView(BaseView, ActionsMixin):
def get_list_row_actions(self): def get_list_row_actions(self):
""" """
Return list of row action objects, each is instance of :class:`~flask_admin.model.template.BaseListRowAction` Return list of row action objects, each is instance of
:class:`~flask_admin.model.template.BaseListRowAction`
""" """
actions = [] actions = []
...@@ -992,11 +999,12 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -992,11 +999,12 @@ class BaseModelView(BaseView, ActionsMixin):
Uses `get_column_names` to get a list of tuples with the model Uses `get_column_names` to get a list of tuples with the model
field name and formatted name for the columns in `column_details_list` field name and formatted name for the columns in `column_details_list`
and not in `column_details_exclude_list`. If `column_details_list` and not in `column_details_exclude_list`. If `column_details_list`
is not set, it will attempt to use the columns from `column_list` is not set, the columns from `scaffold_list_columns` will be used.
or finally the columns from `scaffold_list_columns` will be used.
""" """
only_columns = (self.column_details_list or self.column_list or try:
self.scaffold_list_columns()) only_columns = self.column_details_list or self.scaffold_list_columns()
except NotImplementedError:
raise Exception('Please define column_details_list')
return self.get_column_names( return self.get_column_names(
only_columns=only_columns, only_columns=only_columns,
...@@ -1254,6 +1262,19 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1254,6 +1262,19 @@ class BaseModelView(BaseView, ActionsMixin):
return DeleteForm return DeleteForm
def get_action_form(self):
"""
Create form class for a model action.
Override to implement customized behavior.
"""
class ActionForm(self.form_base_class):
action = HiddenField()
url = HiddenField()
# rowid is retrieved using getlist, for backward compatibility
return ActionForm
def create_form(self, obj=None): def create_form(self, obj=None):
""" """
Instantiate model creation form and return it. Instantiate model creation form and return it.
...@@ -1295,6 +1316,14 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1295,6 +1316,14 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
return self._list_form_class(get_form_data(), obj=obj) return self._list_form_class(get_form_data(), obj=obj)
def action_form(self, obj=None):
"""
Instantiate model action form and return it.
Override to implement custom behavior.
"""
return self._action_form_class(get_form_data(), obj=obj)
def validate_form(self, form): def validate_form(self, form):
""" """
Validate the form on submit. Validate the form on submit.
...@@ -1540,7 +1569,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1540,7 +1569,7 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
pass pass
def on_form_prefill (self, form, id): def on_form_prefill(self, form, id):
""" """
Perform additional actions to pre-fill the edit form. Perform additional actions to pre-fill the edit form.
...@@ -1659,6 +1688,24 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1659,6 +1688,24 @@ class BaseModelView(BaseView, ActionsMixin):
search=request.args.get('search', None), search=request.args.get('search', None),
filters=self._get_list_filter_args()) filters=self._get_list_filter_args())
def _get_filters(self, filters):
"""
Get active filters as dictionary of URL arguments and values
:param filters:
List of filters from ViewArgs object
"""
kwargs = {}
if filters:
for i, pair in enumerate(filters):
idx, flt_name, value = pair
key = 'flt%d_%s' % (i, self.get_filter_arg(idx, self._filters[idx]))
kwargs[key] = value
return kwargs
# URL generation helpers # URL generation helpers
def _get_list_url(self, view_args): def _get_list_url(self, view_args):
""" """
...@@ -1679,12 +1726,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1679,12 +1726,7 @@ class BaseModelView(BaseView, ActionsMixin):
if view_args.page_size: if view_args.page_size:
kwargs['page_size'] = view_args.page_size kwargs['page_size'] = view_args.page_size
if view_args.filters: kwargs.update(self._get_filters(view_args.filters))
for i, pair in enumerate(view_args.filters):
idx, flt_name, value = pair
key = 'flt%d_%s' % (i, self.get_filter_arg(idx, self._filters[idx]))
kwargs[key] = value
return self.get_url('.index_view', **kwargs) return self.get_url('.index_view', **kwargs)
...@@ -1874,6 +1916,10 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1874,6 +1916,10 @@ class BaseModelView(BaseView, ActionsMixin):
# Actions # Actions
actions, actions_confirmation = self.get_actions_list() actions, actions_confirmation = self.get_actions_list()
if actions:
action_form = self.action_form()
else:
action_form = None
clear_search_url = self._get_list_url(view_args.clone(page=0, clear_search_url = self._get_list_url(view_args.clone(page=0,
sort=view_args.sort, sort=view_args.sort,
...@@ -1886,6 +1932,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1886,6 +1932,7 @@ class BaseModelView(BaseView, ActionsMixin):
data=data, data=data,
list_forms=list_forms, list_forms=list_forms,
delete_form=delete_form, delete_form=delete_form,
action_form=action_form,
# List # List
list_columns=self._list_columns, list_columns=self._list_columns,
...@@ -1901,6 +1948,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1901,6 +1948,7 @@ class BaseModelView(BaseView, ActionsMixin):
page_size_url=page_size_url, page_size_url=page_size_url,
page=view_args.page, page=view_args.page,
page_size=page_size, page_size=page_size,
default_page_size=self.page_size,
# Sorting # Sorting
sort_column=view_args.sort, sort_column=view_args.sort,
...@@ -1916,6 +1964,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1916,6 +1964,7 @@ class BaseModelView(BaseView, ActionsMixin):
filters=self._filters, filters=self._filters,
filter_groups=self._get_filter_groups(), filter_groups=self._get_filter_groups(),
active_filters=view_args.filters, active_filters=view_args.filters,
filter_args=self._get_filters(view_args.filters),
# Actions # Actions
actions=actions, actions=actions,
......
...@@ -248,10 +248,10 @@ class BaseTimeBetweenFilter(BaseFilter): ...@@ -248,10 +248,10 @@ class BaseTimeBetweenFilter(BaseFilter):
def clean(self, value): def clean(self, value):
timetuples = [time.strptime(range, '%H:%M:%S') timetuples = [time.strptime(range, '%H:%M:%S')
for range in value.split(' to ')] for range in value.split(' to ')]
return [datetime.time(timetuple.tm_hour, return [
timetuple.tm_min, datetime.time(timetuple.tm_hour, timetuple.tm_min, timetuple.tm_sec)
timetuple.tm_sec) for timetuple in timetuples
for timetuple in timetuples] ]
def operation(self): def operation(self):
return lazy_gettext('between') return lazy_gettext('between')
......
...@@ -9,6 +9,7 @@ def prettify_name(name): ...@@ -9,6 +9,7 @@ def prettify_name(name):
""" """
return name.replace('_', ' ').title() return name.replace('_', ' ').title()
def get_mdict_item_or_list(mdict, key): def get_mdict_item_or_list(mdict, key):
""" """
Return the value for the given key of the multidict. Return the value for the given key of the multidict.
......
...@@ -126,4 +126,3 @@ def macro(name): ...@@ -126,4 +126,3 @@ def macro(name):
return m(model=model, column=column) return m(model=model, column=column)
return inner return inner
...@@ -14,11 +14,13 @@ ...@@ -14,11 +14,13 @@
{% macro form(actions, url) %} {% macro form(actions, url) %}
{% if actions %} {% if actions %}
<form id="action_form" action="{{ url }}" method="POST" style="display: none"> <form id="action_form" action="{{ url }}" method="POST" style="display: none">
{% if csrf_token %} {% if action_form.csrf_token %}
{{ action_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %} {% endif %}
<input type="hidden" name="url" value="{{ return_url }}"> {{ action_form.url(value=return_url) }}
<input type="hidden" id="action" name="action" /> {{ action_form.action() }}
</form> </form>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
......
...@@ -34,6 +34,18 @@ ...@@ -34,6 +34,18 @@
{% macro filter_form() %} {% macro filter_form() %}
<form id="filter_form" method="GET" action="{{ return_url }}"> <form id="filter_form" method="GET" action="{{ return_url }}">
{% if sort_column is not none %}
<input type="hidden" name="sort" value="{{ sort_column }}">
{% endif %}
{% if sort_desc %}
<input type="hidden" name="desc" value="{{ sort_desc }}">
{% endif %}
{% if search %}
<input type="hidden" name="search" value="{{ search }}">
{% endif %}
{% if page_size != default_page_size %}
<input type="hidden" name="page_size" value="{{ page_size }}">
{% endif %}
<div class="pull-right"> <div class="pull-right">
<button type="submit" class="btn btn-primary" style="display: none">{{ _gettext('Apply') }}</button> <button type="submit" class="btn btn-primary" style="display: none">{{ _gettext('Apply') }}</button>
{% if active_filters %} {% if active_filters %}
...@@ -48,6 +60,12 @@ ...@@ -48,6 +60,12 @@
{% macro search_form(input_class="span2") %} {% macro search_form(input_class="span2") %}
<form method="GET" action="{{ return_url }}" class="search-form"> <form method="GET" action="{{ return_url }}" class="search-form">
{% for flt_name, flt_value in filter_args.items() %}
<input type="hidden" name="{{ flt_name }}" value="{{ flt_value }}">
{% endfor %}
{% if page_size != default_page_size %}
<input type="hidden" name="page_size" value="{{ page_size }}">
{% endif %}
{% if sort_column is not none %} {% if sort_column is not none %}
<input type="hidden" name="sort" value="{{ sort_column }}"> <input type="hidden" name="sort" value="{{ sort_column }}">
{% endif %} {% endif %}
......
...@@ -82,8 +82,8 @@ ...@@ -82,8 +82,8 @@
<th class="span1">&nbsp;</th> <th class="span1">&nbsp;</th>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% set column = 0 %}
{% for c, name in list_columns %} {% for c, name in list_columns %}
{% set column = loop.index0 %}
<th class="column-header col-{{c}}"> <th class="column-header col-{{c}}">
{% if admin_view.is_sortable(c) %} {% if admin_view.is_sortable(c) %}
{% if sort_column == column %} {% if sort_column == column %}
...@@ -108,7 +108,6 @@ ...@@ -108,7 +108,6 @@
></a> ></a>
{% endif %} {% endif %}
</th> </th>
{% set column = column + 1 %}
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
</tr> </tr>
......
...@@ -14,11 +14,13 @@ ...@@ -14,11 +14,13 @@
{% macro form(actions, url) %} {% macro form(actions, url) %}
{% if actions %} {% if actions %}
<form id="action_form" action="{{ url }}" method="POST" style="display: none"> <form id="action_form" action="{{ url }}" method="POST" style="display: none">
{% if csrf_token %} {% if action_form.csrf_token %}
{{ action_form.csrf_token }}
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %} {% endif %}
<input type="hidden" name="url" value="{{ return_url }}"> {{ action_form.url(value=return_url) }}
<input type="hidden" id="action" name="action" /> {{ action_form.action() }}
</form> </form>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
......
...@@ -34,6 +34,18 @@ ...@@ -34,6 +34,18 @@
{% macro filter_form() %} {% macro filter_form() %}
<form id="filter_form" method="GET" action="{{ return_url }}"> <form id="filter_form" method="GET" action="{{ return_url }}">
{% if sort_column is not none %}
<input type="hidden" name="sort" value="{{ sort_column }}">
{% endif %}
{% if sort_desc %}
<input type="hidden" name="desc" value="{{ sort_desc }}">
{% endif %}
{% if search %}
<input type="hidden" name="search" value="{{ search }}">
{% endif %}
{% if page_size != default_page_size %}
<input type="hidden" name="page_size" value="{{ page_size }}">
{% endif %}
<div class="pull-right"> <div class="pull-right">
<button type="submit" class="btn btn-primary" style="display: none">{{ _gettext('Apply') }}</button> <button type="submit" class="btn btn-primary" style="display: none">{{ _gettext('Apply') }}</button>
{% if active_filters %} {% if active_filters %}
...@@ -48,6 +60,12 @@ ...@@ -48,6 +60,12 @@
{% macro search_form(input_class="col-md-2") %} {% macro search_form(input_class="col-md-2") %}
<form method="GET" action="{{ return_url }}" class="navbar-form navbar-left" role="search"> <form method="GET" action="{{ return_url }}" class="navbar-form navbar-left" role="search">
{% for flt_name, flt_value in filter_args.items() %}
<input type="hidden" name="{{ flt_name }}" value="{{ flt_value }}">
{% endfor %}
{% if page_size != default_page_size %}
<input type="hidden" name="page_size" value="{{ page_size }}">
{% endif %}
{% if sort_column is not none %} {% if sort_column is not none %}
<input type="hidden" name="sort" value="{{ sort_column }}"> <input type="hidden" name="sort" value="{{ sort_column }}">
{% endif %} {% endif %}
......
...@@ -81,8 +81,8 @@ ...@@ -81,8 +81,8 @@
<th class="col-md-1">&nbsp;</th> <th class="col-md-1">&nbsp;</th>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% set column = 0 %}
{% for c, name in list_columns %} {% for c, name in list_columns %}
{% set column = loop.index0 %}
<th class="column-header col-{{c}}"> <th class="column-header col-{{c}}">
{% if admin_view.is_sortable(c) %} {% if admin_view.is_sortable(c) %}
{% if sort_column == column %} {% if sort_column == column %}
...@@ -107,7 +107,6 @@ ...@@ -107,7 +107,6 @@
></a> ></a>
{% endif %} {% endif %}
</th> </th>
{% set column = column + 1 %}
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
</tr> </tr>
...@@ -202,5 +201,22 @@ ...@@ -202,5 +201,22 @@
); );
{% endif %} {% endif %}
})(jQuery); })(jQuery);
// Catch exception when closing dialog with <esc> key
// and prevent accidental deletions.
function safeConfirm(msg) {
try {
var isconfirmed = confirm(msg);
if (isconfirmed == true) {
return true;
}
else {
return false;
}
}
catch(err) {
return false;
}
}
</script> </script>
{% endblock %} {% endblock %}
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
{% elif csrf_token %} {% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %} {% endif %}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="Delete record"> <button onclick="return safeConfirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="Delete record">
<span class="fa fa-trash glyphicon glyphicon-trash"></span> <span class="fa fa-trash glyphicon glyphicon-trash"></span>
</button> </button>
</form> </form>
......
...@@ -134,6 +134,7 @@ def test_file_admin(): ...@@ -134,6 +134,7 @@ def test_file_admin():
ok_('path=dummy_renamed_dir' not in rv.data.decode('utf-8')) ok_('path=dummy_renamed_dir' not in rv.data.decode('utf-8'))
ok_('path=dummy.txt' in rv.data.decode('utf-8')) ok_('path=dummy.txt' in rv.data.decode('utf-8'))
def test_modal_edit(): def test_modal_edit():
# bootstrap 2 - test edit_modal # bootstrap 2 - test edit_modal
app_bs2 = Flask(__name__) app_bs2 = Flask(__name__)
......
...@@ -64,7 +64,8 @@ def test_model(): ...@@ -64,7 +64,8 @@ def test_model():
"name": "test1", "name": "test1",
"point": '{"type": "Point", "coordinates": [125.8, 10.0]}', "point": '{"type": "Point", "coordinates": [125.8, 10.0]}',
"line": '{"type": "LineString", "coordinates": [[50.2345, 94.2], [50.21, 94.87]]}', "line": '{"type": "LineString", "coordinates": [[50.2345, 94.2], [50.21, 94.87]]}',
"polygon": '{"type": "Polygon", "coordinates": [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]}', "polygon": ('{"type": "Polygon", "coordinates": [[[100.0, 0.0], [101.0, 0.0],'
' [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]}'),
"multi": '{"type": "MultiPoint", "coordinates": [[100.0, 0.0], [101.0, 1.0]]}', "multi": '{"type": "MultiPoint", "coordinates": [[100.0, 0.0], [101.0, 1.0]]}',
}) })
eq_(rv.status_code, 302) eq_(rv.status_code, 302)
......
from nose.plugins.skip import SkipTest
from wtforms import __version__ as wtforms_version
# Skip test on PY3
from flask_admin._compat import PY2
if not PY2:
raise SkipTest('MongoEngine is not Python 3 compatible')
if int(wtforms_version[0]) < 2:
raise SkipTest('MongoEngine does not support WTForms 1.')
from flask import Flask from flask import Flask
from flask_admin import Admin from flask_admin import Admin
from flask_mongoengine import MongoEngine from flask_mongoengine import MongoEngine
......
from nose.tools import eq_, ok_ from nose.tools import eq_, ok_
from nose.plugins.skip import SkipTest
# Skip test on PY3
from flask_admin._compat import PY2, as_unicode
if not PY2:
raise SkipTest('MongoEngine is not Python 3 compatible')
from wtforms import fields, validators from wtforms import fields, validators
from flask_admin import form from flask_admin import form
from flask_admin._compat import as_unicode
from flask_admin.contrib.mongoengine import ModelView from flask_admin.contrib.mongoengine import ModelView
from . import setup from . import setup
from datetime import datetime from datetime import datetime
class CustomModelView(ModelView): class CustomModelView(ModelView):
def __init__(self, model, def __init__(self, model,
name=None, category=None, endpoint=None, url=None, name=None, category=None, endpoint=None, url=None,
...@@ -65,8 +61,8 @@ def fill_db(Model1, Model2): ...@@ -65,8 +61,8 @@ def fill_db(Model1, Model2):
Model2('string_field_val_4', 9000, 75.5).save() Model2('string_field_val_4', 9000, 75.5).save()
Model2('string_field_val_5', 6169453081680413441).save() Model2('string_field_val_5', 6169453081680413441).save()
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save() Model1('datetime_obj1', datetime_field=datetime(2014, 4, 3, 1, 9, 0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save() Model1('datetime_obj2', datetime_field=datetime(2013, 3, 2, 0, 8, 0)).save()
def test_model(): def test_model():
...@@ -161,7 +157,7 @@ def test_column_editable_list(): ...@@ -161,7 +157,7 @@ def test_column_editable_list():
ok_('data-role="x-editable"' in data) ok_('data-role="x-editable"' in data)
# Form - Test basic in-line edit functionality # Form - Test basic in-line edit functionality
obj1 = Model1.objects.get(test1 = 'test1_val_3') obj1 = Model1.objects.get(test1='test1_val_3')
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'list_form_pk': str(obj1.id), 'list_form_pk': str(obj1.id),
'test1': 'change-success-1', 'test1': 'change-success-1',
...@@ -175,7 +171,7 @@ def test_column_editable_list(): ...@@ -175,7 +171,7 @@ def test_column_editable_list():
ok_('change-success-1' in data) ok_('change-success-1' in data)
# Test validation error # Test validation error
obj2 = Model1.objects.get(test1 = 'datetime_obj1') obj2 = Model1.objects.get(test1='datetime_obj1')
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'list_form_pk': str(obj2.id), 'list_form_pk': str(obj2.id),
'datetime_field': 'problematic-input', 'datetime_field': 'problematic-input',
...@@ -202,7 +198,7 @@ def test_column_editable_list(): ...@@ -202,7 +198,7 @@ def test_column_editable_list():
view = CustomModelView(Model2, column_editable_list=['model1']) view = CustomModelView(Model2, column_editable_list=['model1'])
admin.add_view(view) admin.add_view(view)
obj3 = Model2.objects.get(string_field = 'string_field_val_1') obj3 = Model2.objects.get(string_field='string_field_val_1')
rv = client.post('/admin/model2/ajax/update/', data={ rv = client.post('/admin/model2/ajax/update/', data={
'list_form_pk': str(obj3.id), 'list_form_pk': str(obj3.id),
'model1': str(obj1.id), 'model1': str(obj1.id),
...@@ -287,7 +283,8 @@ def test_column_filters(): ...@@ -287,7 +283,8 @@ def test_column_filters():
eq_(len(view._filters), 7) eq_(len(view._filters), 7)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Test1']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Test1']],
[ [
(0, 'contains'), (0, 'contains'),
(1, 'not contains'), (1, 'not contains'),
...@@ -296,7 +293,8 @@ def test_column_filters(): ...@@ -296,7 +293,8 @@ def test_column_filters():
(4, 'empty'), (4, 'empty'),
(5, 'in list'), (5, 'in list'),
(6, 'not in list'), (6, 'not in list'),
]) ]
)
# Make some test clients # Make some test clients
client = app.test_client() client = app.test_client()
...@@ -367,7 +365,8 @@ def test_column_filters(): ...@@ -367,7 +365,8 @@ def test_column_filters():
view = CustomModelView(Model2, column_filters=['int_field']) view = CustomModelView(Model2, column_filters=['int_field'])
admin.add_view(view) admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']],
[ [
(0, 'equals'), (0, 'equals'),
(1, 'not equal'), (1, 'not equal'),
...@@ -376,7 +375,8 @@ def test_column_filters(): ...@@ -376,7 +375,8 @@ def test_column_filters():
(4, 'empty'), (4, 'empty'),
(5, 'in list'), (5, 'in list'),
(6, 'not in list'), (6, 'not in list'),
]) ]
)
# integer - equals # integer - equals
rv = client.get('/admin/model2/?flt0_0=5000') rv = client.get('/admin/model2/?flt0_0=5000')
...@@ -473,11 +473,13 @@ def test_column_filters(): ...@@ -473,11 +473,13 @@ def test_column_filters():
endpoint="_bools") endpoint="_bools")
admin.add_view(view) admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Bool Field']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Bool Field']],
[ [
(0, 'equals'), (0, 'equals'),
(1, 'not equal'), (1, 'not equal'),
]) ]
)
# boolean - equals - Yes # boolean - equals - Yes
rv = client.get('/admin/_bools/?flt0_0=1') rv = client.get('/admin/_bools/?flt0_0=1')
...@@ -485,7 +487,6 @@ def test_column_filters(): ...@@ -485,7 +487,6 @@ def test_column_filters():
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('string_field_val_1' in data) ok_('string_field_val_1' in data)
ok_('string_field_val_2' not in data) ok_('string_field_val_2' not in data)
#ok_('string_field_val_3' not in data)
# boolean - equals - No # boolean - equals - No
rv = client.get('/admin/_bools/?flt0_0=0') rv = client.get('/admin/_bools/?flt0_0=0')
...@@ -493,7 +494,6 @@ def test_column_filters(): ...@@ -493,7 +494,6 @@ def test_column_filters():
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('string_field_val_1' not in data) ok_('string_field_val_1' not in data)
ok_('string_field_val_2' in data) ok_('string_field_val_2' in data)
#ok_('string_field_val_3' in data)
# boolean - not equals - Yes # boolean - not equals - Yes
rv = client.get('/admin/_bools/?flt0_1=1') rv = client.get('/admin/_bools/?flt0_1=1')
...@@ -501,7 +501,6 @@ def test_column_filters(): ...@@ -501,7 +501,6 @@ def test_column_filters():
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('string_field_val_1' not in data) ok_('string_field_val_1' not in data)
ok_('string_field_val_2' in data) ok_('string_field_val_2' in data)
#ok_('string_field_val_3' in data)
# boolean - not equals - No # boolean - not equals - No
rv = client.get('/admin/_bools/?flt0_1=0') rv = client.get('/admin/_bools/?flt0_1=0')
...@@ -509,14 +508,14 @@ def test_column_filters(): ...@@ -509,14 +508,14 @@ def test_column_filters():
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('string_field_val_1' in data) ok_('string_field_val_1' in data)
ok_('string_field_val_2' not in data) ok_('string_field_val_2' not in data)
#ok_('string_field_val_3' not in data)
# Test float filter # Test float filter
view = CustomModelView(Model2, column_filters=['float_field'], view = CustomModelView(Model2, column_filters=['float_field'],
endpoint="_float") endpoint="_float")
admin.add_view(view) admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Float Field']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Float Field']],
[ [
(0, 'equals'), (0, 'equals'),
(1, 'not equal'), (1, 'not equal'),
...@@ -525,7 +524,8 @@ def test_column_filters(): ...@@ -525,7 +524,8 @@ def test_column_filters():
(4, 'empty'), (4, 'empty'),
(5, 'in list'), (5, 'in list'),
(6, 'not in list'), (6, 'not in list'),
]) ]
)
# float - equals # float - equals
rv = client.get('/admin/_float/?flt0_0=25.9') rv = client.get('/admin/_float/?flt0_0=25.9')
...@@ -609,7 +609,8 @@ def test_column_filters(): ...@@ -609,7 +609,8 @@ def test_column_filters():
endpoint="_datetime") endpoint="_datetime")
admin.add_view(view) admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']],
[ [
(0, 'equals'), (0, 'equals'),
(1, 'not equal'), (1, 'not equal'),
...@@ -618,7 +619,8 @@ def test_column_filters(): ...@@ -618,7 +619,8 @@ def test_column_filters():
(4, 'between'), (4, 'between'),
(5, 'not between'), (5, 'not between'),
(6, 'empty'), (6, 'empty'),
]) ]
)
# datetime - equals # datetime - equals
rv = client.get('/admin/_datetime/?flt0_0=2014-04-03+01%3A09%3A00') rv = client.get('/admin/_datetime/?flt0_0=2014-04-03+01%3A09%3A00')
...@@ -678,6 +680,7 @@ def test_column_filters(): ...@@ -678,6 +680,7 @@ def test_column_filters():
ok_('datetime_obj1' in data) ok_('datetime_obj1' in data)
ok_('datetime_obj2' in data) ok_('datetime_obj2' in data)
def test_default_sort(): def test_default_sort():
app, db, admin = setup() app, db, admin = setup()
M1, _ = create_models(db) M1, _ = create_models(db)
...@@ -784,7 +787,7 @@ def test_subdocument_config(): ...@@ -784,7 +787,7 @@ def test_subdocument_config():
# Check only # Check only
view1 = CustomModelView( view1 = CustomModelView(
Model1, Model1,
form_subdocuments = { form_subdocuments={
'subdoc': { 'subdoc': {
'form_columns': ('name',) 'form_columns': ('name',)
} }
...@@ -800,7 +803,7 @@ def test_subdocument_config(): ...@@ -800,7 +803,7 @@ def test_subdocument_config():
# Check exclude # Check exclude
view2 = CustomModelView( view2 = CustomModelView(
Model1, Model1,
form_subdocuments = { form_subdocuments={
'subdoc': { 'subdoc': {
'form_excluded_columns': ('value',) 'form_excluded_columns': ('value',)
} }
...@@ -831,7 +834,7 @@ def test_subdocument_class_config(): ...@@ -831,7 +834,7 @@ def test_subdocument_class_config():
# Check only # Check only
view1 = CustomModelView( view1 = CustomModelView(
Model1, Model1,
form_subdocuments = { form_subdocuments={
'subdoc': EmbeddedConfig() 'subdoc': EmbeddedConfig()
} }
) )
...@@ -859,7 +862,7 @@ def test_nested_subdocument_config(): ...@@ -859,7 +862,7 @@ def test_nested_subdocument_config():
view1 = CustomModelView( view1 = CustomModelView(
Model1, Model1,
form_subdocuments = { form_subdocuments={
'nested': { 'nested': {
'form_subdocuments': { 'form_subdocuments': {
'comment': { 'comment': {
...@@ -889,7 +892,7 @@ def test_nested_list_subdocument(): ...@@ -889,7 +892,7 @@ def test_nested_list_subdocument():
# Check only # Check only
view1 = CustomModelView( view1 = CustomModelView(
Model1, Model1,
form_subdocuments = { form_subdocuments={
'subdoc': { 'subdoc': {
'form_subdocuments': { 'form_subdocuments': {
None: { None: {
...@@ -922,14 +925,13 @@ def test_nested_sortedlist_subdocument(): ...@@ -922,14 +925,13 @@ def test_nested_sortedlist_subdocument():
# Check only # Check only
view1 = CustomModelView( view1 = CustomModelView(
Model1, Model1,
form_subdocuments = { form_subdocuments={
'subdoc': { 'subdoc': {
'form_subdocuments': { 'form_subdocuments': {
None: { None: {
'form_columns': ('name',) 'form_columns': ('name',)
} }
} }
} }
} }
) )
...@@ -965,6 +967,7 @@ def test_sortedlist_subdocument_validation(): ...@@ -965,6 +967,7 @@ def test_sortedlist_subdocument_validation():
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
ok_('This field is required' in rv.data) ok_('This field is required' in rv.data)
def test_list_subdocument_validation(): def test_list_subdocument_validation():
app, db, admin = setup() app, db, admin = setup()
...@@ -989,6 +992,7 @@ def test_list_subdocument_validation(): ...@@ -989,6 +992,7 @@ def test_list_subdocument_validation():
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
ok_('This field is required' in rv.data) ok_('This field is required' in rv.data)
def test_ajax_fk(): def test_ajax_fk():
app, db, admin = setup() app, db, admin = setup()
...@@ -1070,7 +1074,7 @@ def test_nested_ajax_refs(): ...@@ -1070,7 +1074,7 @@ def test_nested_ajax_refs():
view1 = CustomModelView( view1 = CustomModelView(
Model1, Model1,
form_subdocuments = { form_subdocuments={
'nested': { 'nested': {
'form_ajax_refs': { 'form_ajax_refs': {
'comment': { 'comment': {
...@@ -1131,7 +1135,7 @@ def test_form_args_embeddeddoc(): ...@@ -1131,7 +1135,7 @@ def test_form_args_embeddeddoc():
view = CustomModelView( view = CustomModelView(
Model, Model,
form_args= { form_args={
'info': {'label': 'Information'}, 'info': {'label': 'Information'},
'timestamp': {'label': 'Last Updated Time'} 'timestamp': {'label': 'Last Updated Time'}
} }
......
...@@ -100,12 +100,12 @@ def fill_db(Model1, Model2): ...@@ -100,12 +100,12 @@ def fill_db(Model1, Model2):
Model2('char_field_val_4', 9000, 75.5).save() Model2('char_field_val_4', 9000, 75.5).save()
Model2('char_field_val_5', 6169453081680413441).save() Model2('char_field_val_5', 6169453081680413441).save()
Model1('date_obj1', date_field=date(2014,11,17)).save() Model1('date_obj1', date_field=date(2014, 11, 17)).save()
Model1('date_obj2', date_field=date(2013,10,16)).save() Model1('date_obj2', date_field=date(2013, 10, 16)).save()
Model1('timeonly_obj1', timeonly_field=time(11,10,9)).save() Model1('timeonly_obj1', timeonly_field=time(11, 10, 9)).save()
Model1('timeonly_obj2', timeonly_field=time(10,9,8)).save() Model1('timeonly_obj2', timeonly_field=time(10, 9, 8)).save()
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save() Model1('datetime_obj1', datetime_field=datetime(2014, 4, 3, 1, 9, 0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save() Model1('datetime_obj2', datetime_field=datetime(2013, 3, 2, 0, 8, 0)).save()
def test_model(): def test_model():
...@@ -216,7 +216,8 @@ def test_column_editable_list(): ...@@ -216,7 +216,8 @@ def test_column_editable_list():
# Test validation error # Test validation error
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'list_form_pk': '1', 'list_form_pk': '1',
'test1': 'longerthantwentycharacterslongerthantwentycharacterslongerthantwentycharacterslongerthantwentycharacters', 'test1': ('longerthantwentycharacterslongerthantwentycharacterslonger'
'thantwentycharacterslongerthantwentycharacters'),
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
eq_(rv.status_code, 500) eq_(rv.status_code, 500)
...@@ -320,7 +321,8 @@ def test_column_filters(): ...@@ -320,7 +321,8 @@ def test_column_filters():
eq_(len(view._filters), 7) eq_(len(view._filters), 7)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Test1']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Test1']],
[ [
(0, 'contains'), (0, 'contains'),
(1, 'not contains'), (1, 'not contains'),
...@@ -329,7 +331,8 @@ def test_column_filters(): ...@@ -329,7 +331,8 @@ def test_column_filters():
(4, 'empty'), (4, 'empty'),
(5, 'in list'), (5, 'in list'),
(6, 'not in list'), (6, 'not in list'),
]) ]
)
# Make some test clients # Make some test clients
client = app.test_client() client = app.test_client()
...@@ -400,7 +403,8 @@ def test_column_filters(): ...@@ -400,7 +403,8 @@ def test_column_filters():
view = CustomModelView(Model2, column_filters=['int_field']) view = CustomModelView(Model2, column_filters=['int_field'])
admin.add_view(view) admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']],
[ [
(0, 'equals'), (0, 'equals'),
(1, 'not equal'), (1, 'not equal'),
...@@ -409,7 +413,8 @@ def test_column_filters(): ...@@ -409,7 +413,8 @@ def test_column_filters():
(4, 'empty'), (4, 'empty'),
(5, 'in list'), (5, 'in list'),
(6, 'not in list'), (6, 'not in list'),
]) ]
)
# integer - equals # integer - equals
rv = client.get('/admin/model2/?flt0_0=5000') rv = client.get('/admin/model2/?flt0_0=5000')
...@@ -506,11 +511,13 @@ def test_column_filters(): ...@@ -506,11 +511,13 @@ def test_column_filters():
endpoint="_bools") endpoint="_bools")
admin.add_view(view) admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Bool Field']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Bool Field']],
[ [
(0, 'equals'), (0, 'equals'),
(1, 'not equal'), (1, 'not equal'),
]) ]
)
# boolean - equals - Yes # boolean - equals - Yes
rv = client.get('/admin/_bools/?flt0_0=1') rv = client.get('/admin/_bools/?flt0_0=1')
...@@ -549,7 +556,8 @@ def test_column_filters(): ...@@ -549,7 +556,8 @@ def test_column_filters():
endpoint="_float") endpoint="_float")
admin.add_view(view) admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Float Field']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Float Field']],
[ [
(0, 'equals'), (0, 'equals'),
(1, 'not equal'), (1, 'not equal'),
...@@ -558,7 +566,8 @@ def test_column_filters(): ...@@ -558,7 +566,8 @@ def test_column_filters():
(4, 'empty'), (4, 'empty'),
(5, 'in list'), (5, 'in list'),
(6, 'not in list'), (6, 'not in list'),
]) ]
)
# float - equals # float - equals
rv = client.get('/admin/_float/?flt0_0=25.9') rv = client.get('/admin/_float/?flt0_0=25.9')
...@@ -642,7 +651,8 @@ def test_column_filters(): ...@@ -642,7 +651,8 @@ def test_column_filters():
endpoint="_datetime") endpoint="_datetime")
admin.add_view(view) admin.add_view(view)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Date Field']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Date Field']],
[ [
(0, 'equals'), (0, 'equals'),
(1, 'not equal'), (1, 'not equal'),
...@@ -651,9 +661,11 @@ def test_column_filters(): ...@@ -651,9 +661,11 @@ def test_column_filters():
(4, 'between'), (4, 'between'),
(5, 'not between'), (5, 'not between'),
(6, 'empty'), (6, 'empty'),
]) ]
)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Datetime Field']],
[ [
(7, 'equals'), (7, 'equals'),
(8, 'not equal'), (8, 'not equal'),
...@@ -662,9 +674,11 @@ def test_column_filters(): ...@@ -662,9 +674,11 @@ def test_column_filters():
(11, 'between'), (11, 'between'),
(12, 'not between'), (12, 'not between'),
(13, 'empty'), (13, 'empty'),
]) ]
)
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Timeonly Field']], eq_(
[(f['index'], f['operation']) for f in view._filter_groups[u'Timeonly Field']],
[ [
(14, 'equals'), (14, 'equals'),
(15, 'not equal'), (15, 'not equal'),
...@@ -673,7 +687,8 @@ def test_column_filters(): ...@@ -673,7 +687,8 @@ def test_column_filters():
(18, 'between'), (18, 'between'),
(19, 'not between'), (19, 'not between'),
(20, 'empty'), (20, 'empty'),
]) ]
)
# date - equals # date - equals
rv = client.get('/admin/_datetime/?flt0_0=2014-11-17') rv = client.get('/admin/_datetime/?flt0_0=2014-11-17')
...@@ -849,6 +864,7 @@ def test_column_filters(): ...@@ -849,6 +864,7 @@ def test_column_filters():
ok_('timeonly_obj1' in data) ok_('timeonly_obj1' in data)
ok_('timeonly_obj2' in data) ok_('timeonly_obj2' in data)
def test_default_sort(): def test_default_sort():
app, db, admin = setup() app, db, admin = setup()
M1, _ = create_models(db) M1, _ = create_models(db)
......
...@@ -15,6 +15,7 @@ def setup(): ...@@ -15,6 +15,7 @@ def setup():
return app, db, admin return app, db, admin
def setup_postgres(): def setup_postgres():
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = '1' app.config['SECRET_KEY'] = '1'
......
This diff is collapsed.
from nose.tools import eq_, ok_, raises from nose.tools import eq_, ok_
from . import setup from . import setup
from .test_basic import CustomModelView, create_models from .test_basic import CustomModelView, create_models
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from nose.tools import eq_, ok_, raises from nose.tools import eq_, ok_
from wtforms import fields from wtforms import fields
...@@ -62,8 +62,8 @@ def test_inline_form(): ...@@ -62,8 +62,8 @@ def test_inline_form():
eq_(User.query.count(), 1) eq_(User.query.count(), 1)
eq_(UserInfo.query.count(), 0) eq_(UserInfo.query.count(), 0)
rv = client.post('/admin/user/new/', data={'name': u'fbar', \ data = {'name': u'fbar', 'info-0-key': 'foo', 'info-0-val': 'bar'}
'info-0-key': 'foo', 'info-0-val' : 'bar'}) rv = client.post('/admin/user/new/', data=data)
eq_(rv.status_code, 302) eq_(rv.status_code, 302)
eq_(User.query.count(), 2) eq_(User.query.count(), 2)
eq_(UserInfo.query.count(), 1) eq_(UserInfo.query.count(), 1)
...@@ -72,15 +72,28 @@ def test_inline_form(): ...@@ -72,15 +72,28 @@ def test_inline_form():
rv = client.get('/admin/user/edit/?id=2') rv = client.get('/admin/user/edit/?id=2')
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
# Edit - update # Edit - update
rv = client.post('/admin/user/edit/?id=2', data={'name': u'barfoo', \ data = {
'info-0-id': 1, 'info-0-key': u'xxx', 'info-0-val':u'yyy'}) 'name': u'barfoo',
'info-0-id': 1,
'info-0-key': u'xxx',
'info-0-val': u'yyy',
}
rv = client.post('/admin/user/edit/?id=2', data=data)
eq_(UserInfo.query.count(), 1) eq_(UserInfo.query.count(), 1)
eq_(UserInfo.query.one().key, u'xxx') eq_(UserInfo.query.one().key, u'xxx')
# Edit - add & delete # Edit - add & delete
rv = client.post('/admin/user/edit/?id=2', data={'name': u'barf', \ data = {
'del-info-0': 'on', 'info-0-id': '1', 'info-0-key': 'yyy', 'info-0-val': 'xxx', 'name': u'barf',
'info-1-id': None, 'info-1-key': u'bar', 'info-1-val' : u'foo'}) 'del-info-0': 'on',
'info-0-id': '1',
'info-0-key': 'yyy',
'info-0-val': 'xxx',
'info-1-id': None,
'info-1-key': u'bar',
'info-1-val': u'foo',
}
rv = client.post('/admin/user/edit/?id=2', data=data)
eq_(rv.status_code, 302) eq_(rv.status_code, 302)
eq_(User.query.count(), 2) eq_(User.query.count(), 2)
eq_(User.query.get(2).name, u'barf') eq_(User.query.get(2).name, u'barf')
...@@ -259,7 +272,7 @@ def test_inline_form_base_class(): ...@@ -259,7 +272,7 @@ def test_inline_form_base_class():
# Set up Admin # Set up Admin
class UserModelView(ModelView): class UserModelView(ModelView):
inline_models = ((UserEmail,{"form_base_class": StubBaseForm}),) inline_models = ((UserEmail, {"form_base_class": StubBaseForm}),)
form_args = { form_args = {
"emails": {"validators": [ItemsRequired()]} "emails": {"validators": [ItemsRequired()]}
} }
......
import json from flask_admin.babel import lazy_gettext
from flask_babelex import Babel
from nose.tools import eq_, ok_, raises, assert_true from nose.tools import eq_, ok_
from speaklater import make_lazy_string
from . import setup from . import setup
from .test_basic import CustomModelView, create_models from .test_basic import CustomModelView, create_models
class Translator:
translate = False
def __call__(self, string):
if self.translate:
return 'Translated: "{0}"'.format(string)
else:
return string
def test_column_label_translation(): def test_column_label_translation():
app, db, admin = setup() app, db, admin = setup()
Model1, _ = create_models(db) Model1, _ = create_models(db)
translated = Translator() app.config['BABEL_DEFAULT_LOCALE'] = 'es'
label = make_lazy_string(translated, 'Column1') Babel(app)
label = lazy_gettext('Name')
view = CustomModelView(Model1, db.session, view = CustomModelView(Model1, db.session,
column_list=['test1', 'test3'], column_list=['test1', 'test3'],
...@@ -31,12 +22,8 @@ def test_column_label_translation(): ...@@ -31,12 +22,8 @@ def test_column_label_translation():
column_filters=('test1',)) column_filters=('test1',))
admin.add_view(view) admin.add_view(view)
translated.translate = True
non_lazy_groups = view._get_filter_groups()
json.dumps(non_lazy_groups) # Filter dict is JSON serializable.
ok_(translated('Column1') in non_lazy_groups) # Label was translated.
client = app.test_client() client = app.test_client()
# Render index with active filter.
rv = client.get('/admin/model1/?flt1_0=test') rv = client.get('/admin/model1/?flt1_0=test')
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
ok_('{"Nombre":' in rv.data.decode('utf-8'))
...@@ -342,7 +342,7 @@ def test_multi_instances_init(): ...@@ -342,7 +342,7 @@ def test_multi_instances_init():
class ManageIndex(base.AdminIndexView): class ManageIndex(base.AdminIndexView):
pass pass
_ = base.Admin(app, index_view=ManageIndex(url='/manage', endpoint='manage')) _ = base.Admin(app, index_view=ManageIndex(url='/manage', endpoint='manage')) # noqa: F841
@raises(Exception) @raises(Exception)
......
...@@ -229,7 +229,6 @@ def test_image_upload_field(): ...@@ -229,7 +229,6 @@ def test_image_upload_field():
eq_(dummy.upload, 'test1.jpg') eq_(dummy.upload, 'test1.jpg')
ok_(op.exists(op.join(path, 'test1.jpg'))) ok_(op.exists(op.join(path, 'test1.jpg')))
# check allowed extensions # check allowed extensions
for extension in ('gif', 'jpg', 'jpeg', 'png', 'tiff'): for extension in ('gif', 'jpg', 'jpeg', 'png', 'tiff'):
filename = 'copyleft.' + extension filename = 'copyleft.' + extension
......
...@@ -424,6 +424,23 @@ def test_csrf(): ...@@ -424,6 +424,23 @@ def test_csrf():
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
ok_(u'Record was successfully deleted.' in rv.data.decode('utf-8')) ok_(u'Record was successfully deleted.' in rv.data.decode('utf-8'))
################
# actions
################
rv = client.get('/admin/secure/')
eq_(rv.status_code, 200)
ok_(u'name="csrf_token"' in rv.data.decode('utf-8'))
csrf_token = get_csrf_token(rv.data.decode('utf-8'))
# Delete without CSRF token, test validation errors
rv = client.post('/admin/secure/action/',
data=dict(rowid='1', url='/admin/secure/', action='delete'),
follow_redirects=True)
eq_(rv.status_code, 200)
ok_(u'Record was successfully deleted.' not in rv.data.decode('utf-8'))
ok_(u'Failed to perform action.' in rv.data.decode('utf-8'))
def test_custom_form(): def test_custom_form():
app, admin = setup() app, admin = setup()
...@@ -562,7 +579,7 @@ def test_export_csv(): ...@@ -562,7 +579,7 @@ def test_export_csv():
# test explicit use of column_export_list # test explicit use of column_export_list
view = MockModelView(Model, view_data, can_export=True, view = MockModelView(Model, view_data, can_export=True,
column_list=['col1', 'col2'], column_list=['col1', 'col2'],
column_export_list=['id','col1','col2'], column_export_list=['id', 'col1', 'col2'],
endpoint='exportinclusion') endpoint='exportinclusion')
admin.add_view(view) admin.add_view(view)
...@@ -612,7 +629,7 @@ def test_export_csv(): ...@@ -612,7 +629,7 @@ def test_export_csv():
view = MockModelView( view = MockModelView(
Model, view_data, can_export=True, column_list=['col1', 'col2'], Model, view_data, can_export=True, column_list=['col1', 'col2'],
column_labels={'col1': 'Str Field', 'col2': 'Int Field'}, column_labels={'col1': 'Str Field', 'col2': 'Int Field'},
column_formatters=dict(col2=lambda v, c, m, p: m.col2*2), column_formatters=dict(col2=lambda v, c, m, p: m.col2 * 2),
endpoint="types_and_formatters" endpoint="types_and_formatters"
) )
admin.add_view(view) admin.add_view(view)
...@@ -630,8 +647,8 @@ def test_export_csv(): ...@@ -630,8 +647,8 @@ def test_export_csv():
view = MockModelView( view = MockModelView(
Model, view_data, can_export=True, column_list=['col1', 'col2'], Model, view_data, can_export=True, column_list=['col1', 'col2'],
column_formatters_export=dict(col2=lambda v, c, m, p: m.col2*3), column_formatters_export=dict(col2=lambda v, c, m, p: m.col2 * 3),
column_formatters=dict(col2=lambda v, c, m, p: m.col2*2), # overridden column_formatters=dict(col2=lambda v, c, m, p: m.col2 * 2), # overridden
column_type_formatters_export=type_formatters, column_type_formatters_export=type_formatters,
endpoint="export_types_and_formatters" endpoint="export_types_and_formatters"
) )
......
...@@ -119,6 +119,7 @@ def iterencode(iter): ...@@ -119,6 +119,7 @@ def iterencode(iter):
.replace(CHAR_SEPARATOR, CHAR_ESCAPE + CHAR_SEPARATOR) .replace(CHAR_SEPARATOR, CHAR_ESCAPE + CHAR_SEPARATOR)
for v in iter) for v in iter)
def iterdecode(value): def iterdecode(value):
""" """
Decode enumerable from string presentation as a tuple Decode enumerable from string presentation as a tuple
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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