Commit 26bb7798 authored by Rad Cirskis's avatar Rad Cirskis

Merge remote-tracking branch 'upstream/master' into missing-extra-args-_get_list_extra_args

parents 9b987b8f 7fa26ab2
...@@ -2,10 +2,6 @@ sudo: false ...@@ -2,10 +2,6 @@ sudo: false
language: python language: python
matrix: matrix:
include: include:
- python: 2.6
env: TOX_ENV=py26-WTForms1
- python: 2.6
env: TOX_ENV=py26-WTForms2
- python: 2.7 - python: 2.7
env: TOX_ENV=py27-WTForms1 env: TOX_ENV=py27-WTForms1
- python: 2.7 - python: 2.7
...@@ -14,10 +10,6 @@ matrix: ...@@ -14,10 +10,6 @@ matrix:
env: TOX_ENV=flake8 env: TOX_ENV=flake8
- python: 2.7 - python: 2.7
env: TOX_ENV=docs-html env: TOX_ENV=docs-html
- python: 3.3
env: TOX_ENV=py33-WTForms1
- python: 3.3
env: TOX_ENV=py33-WTForms2
- python: 3.4 - python: 3.4
env: TOX_ENV=py34-WTForms1 env: TOX_ENV=py34-WTForms1
- python: 3.4 - python: 3.4
......
...@@ -93,6 +93,7 @@ For all the tests to pass successfully, you'll need Postgres & MongoDB to be run ...@@ -93,6 +93,7 @@ For all the tests to pass successfully, you'll need Postgres & MongoDB to be run
CREATE DATABASE flask_admin_test; CREATE DATABASE flask_admin_test;
CREATE EXTENSION postgis; CREATE EXTENSION postgis;
CREATE EXTENSION hstore;
You can also run the tests on multiple environments using *tox*. You can also run the tests on multiple environments using *tox*.
......
...@@ -41,7 +41,7 @@ Support ...@@ -41,7 +41,7 @@ Support
**** ****
Python 2.6 - 2.7 and 3.3 - 3.4. Python 2.7 and 3.3 or higher.
Indices And Tables Indices And Tables
------------------ ------------------
......
...@@ -80,6 +80,9 @@ are a few different ways of approaching this. ...@@ -80,6 +80,9 @@ are a few different ways of approaching this.
HTTP Basic Auth HTTP Basic Auth
--------------- ---------------
Unfortunately, there is no easy way of applying HTTP Basic Auth just to your admin
interface.
The simplest form of authentication is HTTP Basic Auth. It doesn't interfere The simplest form of authentication is HTTP Basic Auth. It doesn't interfere
with your database models, and it doesn't require you to write any new view logic or with your database models, and it doesn't require you to write any new view logic or
template code. So it's great for when you're deploying something that's still template code. So it's great for when you're deploying something that's still
...@@ -88,9 +91,6 @@ under development, before you want the whole world to see it. ...@@ -88,9 +91,6 @@ under development, before you want the whole world to see it.
Have a look at `Flask-BasicAuth <https://flask-basicauth.readthedocs.io/>`_ to see just how Have a look at `Flask-BasicAuth <https://flask-basicauth.readthedocs.io/>`_ to see just how
easy it is to put your whole application behind HTTP Basic Auth. easy it is to put your whole application behind HTTP Basic Auth.
Unfortunately, there is no easy way of applying HTTP Basic Auth just to your admin
interface.
Rolling Your Own Rolling Your Own
---------------- ----------------
For a more flexible solution, Flask-Admin lets you define access control rules For a more flexible solution, Flask-Admin lets you define access control rules
......
...@@ -32,12 +32,17 @@ class User(db.Model): ...@@ -32,12 +32,17 @@ class User(db.Model):
password = db.Column(db.String(64)) password = db.Column(db.String(64))
# Flask-Login integration # Flask-Login integration
# NOTE: is_authenticated, is_active, and is_anonymous
# are methods in Flask-Login < 0.3.0
@property
def is_authenticated(self): def is_authenticated(self):
return True return True
@property
def is_active(self): def is_active(self):
return True return True
@property
def is_anonymous(self): def is_anonymous(self):
return False return False
......
...@@ -27,12 +27,17 @@ class User(db.Document): ...@@ -27,12 +27,17 @@ class User(db.Document):
password = db.StringField(max_length=64) password = db.StringField(max_length=64)
# Flask-Login integration # Flask-Login integration
# NOTE: is_authenticated, is_active, and is_anonymous
# are methods in Flask-Login < 0.3.0
@property
def is_authenticated(self): def is_authenticated(self):
return True return True
@property
def is_active(self): def is_active(self):
return True return True
@property
def is_anonymous(self): def is_anonymous(self):
return False return False
......
...@@ -8,10 +8,14 @@ from .widgets import LeafletWidget ...@@ -8,10 +8,14 @@ from .widgets import LeafletWidget
class GeoJSONField(JSONField): class GeoJSONField(JSONField):
widget = LeafletWidget()
def __init__(self, label=None, validators=None, geometry_type="GEOMETRY", def __init__(self, label=None, validators=None, geometry_type="GEOMETRY",
srid='-1', session=None, **kwargs): srid='-1', session=None, tile_layer_url=None,
tile_layer_attribution=None, **kwargs):
self.widget = LeafletWidget(
tile_layer_url=tile_layer_url,
tile_layer_attribution=tile_layer_attribution
)
super(GeoJSONField, self).__init__(label, validators, **kwargs) super(GeoJSONField, self).__init__(label, validators, **kwargs)
self.web_srid = 4326 self.web_srid = 4326
self.srid = srid self.srid = srid
......
...@@ -9,4 +9,6 @@ class AdminModelConverter(SQLAAdminConverter): ...@@ -9,4 +9,6 @@ class AdminModelConverter(SQLAAdminConverter):
field_args['geometry_type'] = column.type.geometry_type field_args['geometry_type'] = column.type.geometry_type
field_args['srid'] = column.type.srid field_args['srid'] = column.type.srid
field_args['session'] = self.session field_args['session'] = self.session
field_args['tile_layer_url'] = self.view.tile_layer_url
field_args['tile_layer_attribution'] = self.view.tile_layer_attribution
return GeoJSONField(**field_args) return GeoJSONField(**field_args)
...@@ -14,6 +14,8 @@ def geom_formatter(view, value): ...@@ -14,6 +14,8 @@ def geom_formatter(view, value):
"data-height": 70, "data-height": 70,
"data-geometry-type": to_shape(value).geom_type, "data-geometry-type": to_shape(value).geom_type,
"data-zoom": 15, "data-zoom": 15,
"data-tile-layer-url": view.tile_layer_url,
"data-tile-layer-attribution": view.tile_layer_attribution
}) })
if value.srid is -1: if value.srid is -1:
value.srid = 4326 value.srid = 4326
......
...@@ -5,3 +5,5 @@ from flask_admin.contrib.geoa import form, typefmt ...@@ -5,3 +5,5 @@ from flask_admin.contrib.geoa import form, typefmt
class ModelView(SQLAModelView): class ModelView(SQLAModelView):
model_form_converter = form.AdminModelConverter model_form_converter = form.AdminModelConverter
column_type_formatters = typefmt.DEFAULT_FORMATTERS column_type_formatters = typefmt.DEFAULT_FORMATTERS
tile_layer_url = None
tile_layer_attribution = None
...@@ -23,7 +23,8 @@ class LeafletWidget(TextArea): ...@@ -23,7 +23,8 @@ class LeafletWidget(TextArea):
""" """
def __init__( def __init__(
self, width='auto', height=350, center=None, self, width='auto', height=350, center=None,
zoom=None, min_zoom=None, max_zoom=None, max_bounds=None): zoom=None, min_zoom=None, max_zoom=None, max_bounds=None,
tile_layer_url=None, tile_layer_attribution=None):
self.width = width self.width = width
self.height = height self.height = height
self.center = center self.center = center
...@@ -31,6 +32,8 @@ class LeafletWidget(TextArea): ...@@ -31,6 +32,8 @@ class LeafletWidget(TextArea):
self.min_zoom = min_zoom self.min_zoom = min_zoom
self.max_zoom = max_zoom self.max_zoom = max_zoom
self.max_bounds = max_bounds self.max_bounds = max_bounds
self.tile_layer_url = tile_layer_url
self.tile_layer_attribution = tile_layer_attribution
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', self.data_role) kwargs.setdefault('data-role', self.data_role)
...@@ -38,6 +41,10 @@ class LeafletWidget(TextArea): ...@@ -38,6 +41,10 @@ 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 self.tile_layer_url:
kwargs['data-tile-layer-url'] = self.tile_layer_url
if self.tile_layer_attribution:
kwargs['data-tile-layer-attribution'] = self.tile_layer_attribution
if "data-width" not in kwargs: if "data-width" not in kwargs:
kwargs["data-width"] = self.width kwargs["data-width"] = self.width
if "data-height" not in kwargs: if "data-height" not in kwargs:
......
from wtforms import fields from wtforms import fields
from peewee import (CharField, DateTimeField, DateField, TimeField, from peewee import (CharField, DateTimeField, DateField, TimeField,
PrimaryKeyField, ForeignKeyField, BaseModel) PrimaryKeyField, ForeignKeyField)
try:
from peewee import BaseModel
except ImportError:
from peewee import ModelBase as BaseModel
from wtfpeewee.orm import ModelConverter, model_form from wtfpeewee.orm import ModelConverter, model_form
...@@ -265,7 +270,10 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -265,7 +270,10 @@ class InlineModelConverter(InlineModelConverterBase):
allow_pk=True, allow_pk=True,
converter=converter) converter=converter)
prop_name = reverse_field.related_name try:
prop_name = reverse_field.related_name
except AttributeError:
prop_name = reverse_field.backref
label = self.get_label(info, prop_name) label = self.get_label(info, prop_name)
......
...@@ -234,14 +234,24 @@ class ModelView(BaseModelView): ...@@ -234,14 +234,24 @@ class ModelView(BaseModelView):
raise Exception('Failed to find field for filter: %s' % name) raise Exception('Failed to find field for filter: %s' % name)
# Check if field is in different model # Check if field is in different model
if attr.model_class != self.model: try:
visible_name = '%s / %s' % (self.get_column_name(attr.model_class.__name__), if attr.model_class != self.model:
self.get_column_name(attr.name)) visible_name = '%s / %s' % (self.get_column_name(attr.model_class.__name__),
else: self.get_column_name(attr.name))
if not isinstance(name, string_types): else:
visible_name = self.get_column_name(attr.name) if not isinstance(name, string_types):
visible_name = self.get_column_name(attr.name)
else:
visible_name = self.get_column_name(name)
except AttributeError:
if attr.model != self.model:
visible_name = '%s / %s' % (self.get_column_name(attr.model.__name__),
self.get_column_name(attr.name))
else: else:
visible_name = self.get_column_name(name) if not isinstance(name, string_types):
visible_name = self.get_column_name(attr.name)
else:
visible_name = self.get_column_name(name)
type_name = type(attr).__name__ type_name = type(attr).__name__
flt = self.filter_converter.convert(type_name, flt = self.filter_converter.convert(type_name,
...@@ -307,12 +317,20 @@ class ModelView(BaseModelView): ...@@ -307,12 +317,20 @@ class ModelView(BaseModelView):
return create_ajax_loader(self.model, name, name, options) return create_ajax_loader(self.model, name, name, options)
def _handle_join(self, query, field, joins): def _handle_join(self, query, field, joins):
if field.model_class != self.model: try:
model_name = field.model_class.__name__ if field.model_class != self.model:
model_name = field.model_class.__name__
if model_name not in joins:
query = query.join(field.model_class, JOIN.LEFT_OUTER)
joins.add(model_name)
except AttributeError:
if field.model != self.model:
model_name = field.model.__name__
if model_name not in joins: if model_name not in joins:
query = query.join(field.model_class, JOIN.LEFT_OUTER) query = query.join(field.model, JOIN.LEFT_OUTER)
joins.add(model_name) joins.add(model_name)
return query return query
...@@ -321,8 +339,12 @@ class ModelView(BaseModelView): ...@@ -321,8 +339,12 @@ class ModelView(BaseModelView):
field = getattr(self.model, sort_field) field = getattr(self.model, sort_field)
query = query.order_by(field.desc() if sort_desc else field.asc()) query = query.order_by(field.desc() if sort_desc else field.asc())
elif isinstance(sort_field, Field): elif isinstance(sort_field, Field):
if sort_field.model_class != self.model: try:
query = self._handle_join(query, sort_field, joins) if sort_field.model_class != self.model:
query = self._handle_join(query, sort_field, joins)
except AttributeError:
if sort_field.model != self.model:
query = self._handle_join(query, sort_field, joins)
query = query.order_by(sort_field.desc() if sort_desc else sort_field.asc()) query = query.order_by(sort_field.desc() if sort_desc else sort_field.asc())
......
...@@ -69,7 +69,7 @@ class QueryAjaxModelLoader(AjaxModelLoader): ...@@ -69,7 +69,7 @@ class QueryAjaxModelLoader(AjaxModelLoader):
query = query.filter(or_(*filters)) query = query.filter(or_(*filters))
if self.filters: if self.filters:
filters = ["%s.%s" % (self.model.__name__.lower(), value) for value in self.filters] filters = ["%s.%s" % (self.model.__tablename__.lower(), value) for value in self.filters]
query = query.filter(and_(*filters)) query = query.filter(and_(*filters))
if self.order_by: if self.order_by:
......
...@@ -296,5 +296,5 @@ class InlineModelFormList(InlineFieldList): ...@@ -296,5 +296,5 @@ class InlineModelFormList(InlineFieldList):
def get_pk_from_identity(obj): def get_pk_from_identity(obj):
# TODO: Remove me # TODO: Remove me
cls, key = identity_key(instance=obj) key = identity_key(instance=obj)[1]
return u':'.join(text_type(x) for x in key) return u':'.join(text_type(x) for x in key)
...@@ -373,7 +373,7 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -373,7 +373,7 @@ class FilterConverter(filters.BaseFilterConverter):
@filters.convert('string', 'char', 'unicode', 'varchar', 'tinytext', @filters.convert('string', 'char', 'unicode', 'varchar', 'tinytext',
'text', 'mediumtext', 'longtext', 'unicodetext', 'text', 'mediumtext', 'longtext', 'unicodetext',
'nchar', 'nvarchar', 'ntext') 'nchar', 'nvarchar', 'ntext', 'citext')
def conv_string(self, column, name, **kwargs): def conv_string(self, column, name, **kwargs):
return [f(column, name, **kwargs) for f in self.strings] return [f(column, name, **kwargs) for f in self.strings]
......
import warnings import warnings
from enum import Enum
from wtforms import fields, validators from wtforms import fields, validators
from sqlalchemy import Boolean, Column from sqlalchemy import Boolean, Column
...@@ -9,7 +10,7 @@ from flask_admin.model.form import (converts, ModelConverterBase, ...@@ -9,7 +10,7 @@ from flask_admin.model.form import (converts, ModelConverterBase,
from flask_admin.model.fields import AjaxSelectField, AjaxSelectMultipleField from flask_admin.model.fields import AjaxSelectField, AjaxSelectMultipleField
from flask_admin.model.helpers import prettify_name from flask_admin.model.helpers import prettify_name
from flask_admin._backwards import get_property from flask_admin._backwards import get_property
from flask_admin._compat import iteritems from flask_admin._compat import iteritems, text_type
from .validators import Unique from .validators import Unique
from .fields import (QuerySelectField, QuerySelectMultipleField, from .fields import (QuerySelectField, QuerySelectMultipleField,
...@@ -154,7 +155,9 @@ class AdminModelConverter(ModelConverterBase): ...@@ -154,7 +155,9 @@ class AdminModelConverter(ModelConverterBase):
if len(prop.columns) > 1: if len(prop.columns) > 1:
columns = filter_foreign_columns(model.__table__, prop.columns) columns = filter_foreign_columns(model.__table__, prop.columns)
if len(columns) > 1: if len(columns) == 0:
return None
elif len(columns) > 1:
warnings.warn('Can not convert multiple-column properties (%s.%s)' % (model, prop.key)) warnings.warn('Can not convert multiple-column properties (%s.%s)' % (model, prop.key))
return None return None
...@@ -279,6 +282,7 @@ class AdminModelConverter(ModelConverterBase): ...@@ -279,6 +282,7 @@ class AdminModelConverter(ModelConverterBase):
accepted_values.append(None) accepted_values.append(None)
field_args['validators'].append(validators.AnyOf(accepted_values)) field_args['validators'].append(validators.AnyOf(accepted_values))
field_args['coerce'] = lambda v: v.name if isinstance(v, Enum) else text_type(v)
return form.Select2Field(**field_args) return form.Select2Field(**field_args)
......
...@@ -420,7 +420,9 @@ class ModelView(BaseModelView): ...@@ -420,7 +420,9 @@ class ModelView(BaseModelView):
if len(p.columns) > 1: if len(p.columns) > 1:
filtered = tools.filter_foreign_columns(self.model.__table__, p.columns) filtered = tools.filter_foreign_columns(self.model.__table__, p.columns)
if len(filtered) > 1: if len(filtered) == 0:
continue
elif len(filtered) > 1:
warnings.warn('Can not convert multiple-column properties (%s.%s)' % (self.model, p.key)) warnings.warn('Can not convert multiple-column properties (%s.%s)' % (self.model, p.key))
continue continue
...@@ -763,6 +765,12 @@ class ModelView(BaseModelView): ...@@ -763,6 +765,12 @@ class ModelView(BaseModelView):
if p.mapper.class_ == self.model: if p.mapper.class_ == self.model:
continue continue
# Check if it is pointing to a differnet bind
source_bind = getattr(self.model, '__bind_key__', None)
target_bind = getattr(p.mapper.class_, '__bind_key__', None)
if source_bind != target_bind:
continue
if p.direction.name in ['MANYTOONE', 'MANYTOMANY']: if p.direction.name in ['MANYTOONE', 'MANYTOMANY']:
relations.add(p.key) relations.add(p.key)
......
...@@ -263,6 +263,16 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -263,6 +263,16 @@ class BaseModelView(BaseView, ActionsMixin):
that macros are not supported. that macros are not supported.
""" """
column_formatters_detail = None
"""
Dictionary of list view column formatters to be used for the detail view.
Defaults to column_formatters when set to None.
Functions the same way as column_formatters except
that macros are not supported.
"""
column_type_formatters = ObsoleteAttr('column_type_formatters', 'list_type_formatters', None) column_type_formatters = ObsoleteAttr('column_type_formatters', 'list_type_formatters', None)
""" """
Dictionary of value type formatters to be used in the list view. Dictionary of value type formatters to be used in the list view.
...@@ -319,6 +329,18 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -319,6 +329,18 @@ class BaseModelView(BaseView, ActionsMixin):
Functions the same way as column_type_formatters. Functions the same way as column_type_formatters.
""" """
column_type_formatters_detail = None
"""
Dictionary of value type formatters to be used in the detail view.
By default, two types are formatted:
1. ``None`` will be displayed as an empty string
2. ``list`` will be joined using ', '
Functions the same way as column_type_formatters.
"""
column_labels = ObsoleteAttr('column_labels', 'rename_columns', None) column_labels = ObsoleteAttr('column_labels', 'rename_columns', None)
""" """
Dictionary where key is column name and value is string to display. Dictionary where key is column name and value is string to display.
...@@ -889,6 +911,9 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -889,6 +911,9 @@ class BaseModelView(BaseView, ActionsMixin):
if self.column_formatters_export is None: if self.column_formatters_export is None:
self.column_formatters_export = self.column_formatters self.column_formatters_export = self.column_formatters
if self.column_formatters_detail is None:
self.column_formatters_detail = self.column_formatters
# Type formatters # Type formatters
if self.column_type_formatters is None: if self.column_type_formatters is None:
self.column_type_formatters = dict(typefmt.BASE_FORMATTERS) self.column_type_formatters = dict(typefmt.BASE_FORMATTERS)
...@@ -896,6 +921,9 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -896,6 +921,9 @@ class BaseModelView(BaseView, ActionsMixin):
if self.column_type_formatters_export is None: if self.column_type_formatters_export is None:
self.column_type_formatters_export = dict(typefmt.EXPORT_FORMATTERS) self.column_type_formatters_export = dict(typefmt.EXPORT_FORMATTERS)
if self.column_type_formatters_detail is None:
self.column_type_formatters_detail = dict(typefmt.DETAIL_FORMATTERS)
if self.column_descriptions is None: if self.column_descriptions is None:
self.column_descriptions = dict() self.column_descriptions = dict()
...@@ -1518,12 +1546,15 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1518,12 +1546,15 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
try: try:
self.on_model_change(form, model, is_created) self.on_model_change(form, model, is_created)
except TypeError: except TypeError as e:
msg = ('%s.on_model_change() now accepts third ' + if re.match(r'on_model_change\(\) takes .* 3 .* arguments .* 4 .* given .*', e.message):
'parameter is_created. Please update your code') % self.model msg = ('%s.on_model_change() now accepts third ' +
warnings.warn(msg) 'parameter is_created. Please update your code') % self.model
warnings.warn(msg)
self.on_model_change(form, model) self.on_model_change(form, model)
else:
raise
def after_model_change(self, form, model, is_created): def after_model_change(self, form, model, is_created):
""" """
...@@ -1808,6 +1839,26 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1808,6 +1839,26 @@ class BaseModelView(BaseView, ActionsMixin):
self.column_type_formatters, self.column_type_formatters,
) )
@contextfunction
def get_detail_value(self, context, model, name):
"""
Returns the value to be displayed in the detail view
:param context:
:py:class:`jinja2.runtime.Context`
:param model:
Model instance
:param name:
Field name
"""
return self._get_list_value(
context,
model,
name,
self.column_formatters_detail,
self.column_type_formatters_detail,
)
def get_export_value(self, model, name): def get_export_value(self, model, name):
""" """
Returns the value to be displayed in export. Returns the value to be displayed in export.
...@@ -2108,7 +2159,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -2108,7 +2159,7 @@ class BaseModelView(BaseView, ActionsMixin):
return self.render(template, return self.render(template,
model=model, model=model,
details_columns=self._details_columns, details_columns=self._details_columns,
get_value=self.get_list_value, get_value=self.get_detail_value,
return_url=return_url) return_url=return_url)
@expose('/delete/', methods=('POST',)) @expose('/delete/', methods=('POST',))
......
...@@ -84,6 +84,13 @@ EXPORT_FORMATTERS = { ...@@ -84,6 +84,13 @@ EXPORT_FORMATTERS = {
dict: dict_formatter, dict: dict_formatter,
} }
DETAIL_FORMATTERS = {
type(None): empty_formatter,
list: list_formatter,
dict: dict_formatter,
}
if Enum is not None: if Enum is not None:
BASE_FORMATTERS[Enum] = enum_formatter BASE_FORMATTERS[Enum] = enum_formatter
EXPORT_FORMATTERS[Enum] = enum_formatter EXPORT_FORMATTERS[Enum] = enum_formatter
DETAIL_FORMATTERS[Enum] = enum_formatter
...@@ -72,7 +72,8 @@ class XEditableWidget(object): ...@@ -72,7 +72,8 @@ class XEditableWidget(object):
field inside of the FieldList (StringField, IntegerField, etc). field inside of the FieldList (StringField, IntegerField, etc).
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs.setdefault('data-value', kwargs.pop('display_value', '')) display_value = kwargs.pop('display_value', '')
kwargs.setdefault('data-value', display_value)
kwargs.setdefault('data-role', 'x-editable') kwargs.setdefault('data-role', 'x-editable')
kwargs.setdefault('data-url', './ajax/update/') kwargs.setdefault('data-url', './ajax/update/')
...@@ -91,7 +92,7 @@ class XEditableWidget(object): ...@@ -91,7 +92,7 @@ class XEditableWidget(object):
return HTMLString( return HTMLString(
'<a %s>%s</a>' % (html_params(**kwargs), '<a %s>%s</a>' % (html_params(**kwargs),
escape(kwargs['data-value'])) escape(display_value))
) )
def get_kwargs(self, field, kwargs): def get_kwargs(self, field, kwargs):
...@@ -104,7 +105,7 @@ class XEditableWidget(object): ...@@ -104,7 +105,7 @@ class XEditableWidget(object):
kwargs['data-type'] = 'textarea' kwargs['data-type'] = 'textarea'
kwargs['data-rows'] = '5' kwargs['data-rows'] = '5'
elif field.type == 'BooleanField': elif field.type == 'BooleanField':
kwargs['data-type'] = 'select' kwargs['data-type'] = 'select2'
# data-source = dropdown options # data-source = dropdown options
kwargs['data-source'] = json.dumps([ kwargs['data-source'] = json.dumps([
{'value': '', 'text': gettext('No')}, {'value': '', 'text': gettext('No')},
...@@ -112,7 +113,7 @@ class XEditableWidget(object): ...@@ -112,7 +113,7 @@ class XEditableWidget(object):
]) ])
kwargs['data-role'] = 'x-editable-boolean' kwargs['data-role'] = 'x-editable-boolean'
elif field.type in ['Select2Field', 'SelectField']: elif field.type in ['Select2Field', 'SelectField']:
kwargs['data-type'] = 'select' kwargs['data-type'] = 'select2'
choices = [{'value': x, 'text': y} for x, y in field.choices] choices = [{'value': x, 'text': y} for x, y in field.choices]
# prepend a blank field to choices if allow_blank = True # prepend a blank field to choices if allow_blank = True
...@@ -144,7 +145,7 @@ class XEditableWidget(object): ...@@ -144,7 +145,7 @@ class XEditableWidget(object):
elif field.type in ['QuerySelectField', 'ModelSelectField', elif field.type in ['QuerySelectField', 'ModelSelectField',
'QuerySelectMultipleField', 'KeyPropertyField']: 'QuerySelectMultipleField', 'KeyPropertyField']:
# QuerySelectField and ModelSelectField are for relations # QuerySelectField and ModelSelectField are for relations
kwargs['data-type'] = 'select' kwargs['data-type'] = 'select2'
choices = [] choices = []
selected_ids = [] selected_ids = []
...@@ -162,12 +163,13 @@ class XEditableWidget(object): ...@@ -162,12 +163,13 @@ class XEditableWidget(object):
kwargs['data-source'] = json.dumps(choices) kwargs['data-source'] = json.dumps(choices)
if field.type == 'QuerySelectMultipleField': if field.type == 'QuerySelectMultipleField':
kwargs['data-type'] = 'select2'
kwargs['data-role'] = 'x-editable-select2-multiple' kwargs['data-role'] = 'x-editable-select2-multiple'
# must use id instead of text or prefilled values won't work # must use id instead of text or prefilled values won't work
separator = getattr(field, 'separator', ',') separator = getattr(field, 'separator', ',')
kwargs['data-value'] = separator.join(selected_ids) kwargs['data-value'] = separator.join(selected_ids)
else:
kwargs['data-value'] = text_type(selected_ids[0])
else: else:
raise Exception('Unsupported field type: %s' % (type(field),)) raise Exception('Unsupported field type: %s' % (type(field),))
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
/* List View - fix gap between actions and table */ /* List View - fix gap between actions and table */
.model-list { .model-list {
position: relative; position: static;
margin-top: -1px; margin-top: -1px;
z-index: 999; z-index: 999;
} }
...@@ -139,3 +139,7 @@ table.filters tr td { ...@@ -139,3 +139,7 @@ table.filters tr td {
*/ */
#no-more-tables td:before { content: attr(data-title); } #no-more-tables td:before { content: attr(data-title); }
} }
.editable-input .select2-container {
min-width: 220px;
}
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
/* List View - fix overlapping border between actions and table */ /* List View - fix overlapping border between actions and table */
.model-list { .model-list {
position: relative; position: static;
margin-top: -1px; margin-top: -1px;
z-index: 999; z-index: 999;
} }
......
...@@ -157,11 +157,19 @@ ...@@ -157,11 +157,19 @@
} }
// set up tiles // set up tiles
var mapboxVersion = window.MAPBOX_ACCESS_TOKEN ? 4 : 3; if($el.data('tile-layer-url')){
L.tileLayer('//{s}.tiles.mapbox.com/v'+mapboxVersion+'/'+MAPBOX_MAP_ID+'/{z}/{x}/{y}.png?access_token='+window.MAPBOX_ACCESS_TOKEN, { var attribution = $el.data('tile-layer-attribution') || ''
attribution: 'Map data &copy; <a href="//openstreetmap.org">OpenStreetMap</a> contributors, <a href="//creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="//mapbox.com">Mapbox</a>', L.tileLayer('//'+$el.data('tile-layer-url'), {
maxZoom: 18 attribution: attribution,
}).addTo(map); maxZoom: 18
}).addTo(map)
} else {
var mapboxVersion = window.MAPBOX_ACCESS_TOKEN ? 4 : 3;
L.tileLayer('//{s}.tiles.mapbox.com/v'+mapboxVersion+'/'+MAPBOX_MAP_ID+'/{z}/{x}/{y}.png?access_token='+window.MAPBOX_ACCESS_TOKEN, {
attribution: 'Map data &copy; <a href="//openstreetmap.org">OpenStreetMap</a> contributors, <a href="//creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="//mapbox.com">Mapbox</a>',
maxZoom: 18
}).addTo(map);
}
// everything below here is to set up editing, so if we're not editable, // everything below here is to set up editing, so if we're not editable,
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
</head> </head>
<body> <body>
{% block page_body %} {% block page_body %}
<div class="container"> <div class="container{%if config.get('FLASK_ADMIN_FLUID_LAYOUT', False) %}-fluid{% endif %}">
<div class="navbar"> <div class="navbar">
<div class="navbar-inner"> <div class="navbar-inner">
{% block brand %} {% block brand %}
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
</head> </head>
<body> <body>
{% block page_body %} {% block page_body %}
<div class="container"> <div class="container{%if config.get('FLASK_ADMIN_FLUID_LAYOUT', False) %}-fluid{% endif %}">
<nav class="navbar navbar-default" role="navigation"> <nav class="navbar navbar-default" role="navigation">
<!-- Brand and toggle get grouped for better mobile display --> <!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header"> <div class="navbar-header">
......
...@@ -2218,6 +2218,34 @@ def test_multipath_joins(): ...@@ -2218,6 +2218,34 @@ def test_multipath_joins():
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
def test_different_bind_joins():
app, db, admin = setup()
app.config['SQLALCHEMY_BINDS'] = {
'other': 'sqlite:///'
}
class Model1(db.Model):
id = db.Column(db.Integer, primary_key=True)
val1 = db.Column(db.String(20))
class Model2(db.Model):
__bind_key__ = 'other'
id = db.Column(db.Integer, primary_key=True)
val1 = db.Column(db.String(20))
first_id = db.Column(db.Integer, db.ForeignKey(Model1.id))
first = db.relationship(Model1)
db.create_all()
view = CustomModelView(Model2, db.session)
admin.add_view(view)
client = app.test_client()
rv = client.get('/admin/model2/')
eq_(rv.status_code, 200)
def test_model_default(): def test_model_default():
app, db, admin = setup() app, db, admin = setup()
_, Model2 = create_models(db) _, Model2 = create_models(db)
......
...@@ -36,9 +36,6 @@ install_requires = [ ...@@ -36,9 +36,6 @@ install_requires = [
'wtforms' 'wtforms'
] ]
if sys.version_info[:2] < (2, 7):
install_requires.append('ordereddict')
setup( setup(
name='Flask-Admin', name='Flask-Admin',
version=grep('__version__'), version=grep('__version__'),
...@@ -76,10 +73,10 @@ setup( ...@@ -76,10 +73,10 @@ setup(
'Programming Language :: Python', 'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
], ],
test_suite='nose.collector' test_suite='nose.collector'
) )
[tox] [tox]
envlist = envlist =
py{26,27,33,34,35,36}-WTForms{1,2} py{27,34,35,36}-WTForms{1,2}
flake8 flake8
docs-html docs-html
skipsdist = true skipsdist = true
......
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