Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
flask-admin
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
JIRA
JIRA
Merge Requests
0
Merge Requests
0
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Python-Dev
flask-admin
Commits
26bb7798
Commit
26bb7798
authored
Jun 28, 2018
by
Rad Cirskis
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'upstream/master' into missing-extra-args-_get_list_extra_args
parents
9b987b8f
7fa26ab2
Changes
29
Hide whitespace changes
Inline
Side-by-side
Showing
29 changed files
with
222 additions
and
63 deletions
+222
-63
.travis.yml
.travis.yml
+0
-8
README.rst
README.rst
+1
-0
index.rst
doc/index.rst
+1
-1
introduction.rst
doc/introduction.rst
+3
-3
app.py
examples/auth-flask-login/app.py
+5
-0
app.py
examples/auth-mongoengine/app.py
+5
-0
fields.py
flask_admin/contrib/geoa/fields.py
+6
-2
form.py
flask_admin/contrib/geoa/form.py
+2
-0
typefmt.py
flask_admin/contrib/geoa/typefmt.py
+2
-0
view.py
flask_admin/contrib/geoa/view.py
+2
-0
widgets.py
flask_admin/contrib/geoa/widgets.py
+8
-1
form.py
flask_admin/contrib/peewee/form.py
+10
-2
view.py
flask_admin/contrib/peewee/view.py
+36
-14
ajax.py
flask_admin/contrib/sqla/ajax.py
+1
-1
fields.py
flask_admin/contrib/sqla/fields.py
+1
-1
filters.py
flask_admin/contrib/sqla/filters.py
+1
-1
form.py
flask_admin/contrib/sqla/form.py
+6
-2
view.py
flask_admin/contrib/sqla/view.py
+9
-1
base.py
flask_admin/model/base.py
+57
-6
typefmt.py
flask_admin/model/typefmt.py
+7
-0
widgets.py
flask_admin/model/widgets.py
+8
-6
admin.css
flask_admin/static/admin/css/bootstrap2/admin.css
+5
-1
admin.css
flask_admin/static/admin/css/bootstrap3/admin.css
+1
-1
form.js
flask_admin/static/admin/js/form.js
+13
-5
base.html
flask_admin/templates/bootstrap2/admin/base.html
+1
-1
base.html
flask_admin/templates/bootstrap3/admin/base.html
+1
-1
test_basic.py
flask_admin/tests/sqla/test_basic.py
+28
-0
setup.py
setup.py
+1
-4
tox.ini
tox.ini
+1
-1
No files found.
.travis.yml
View file @
26bb7798
...
@@ -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
...
...
README.rst
View file @
26bb7798
...
@@ -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*.
...
...
doc/index.rst
View file @
26bb7798
...
@@ -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
------------------
------------------
...
...
doc/introduction.rst
View file @
26bb7798
...
@@ -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
...
...
examples/auth-flask-login/app.py
View file @
26bb7798
...
@@ -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
...
...
examples/auth-mongoengine/app.py
View file @
26bb7798
...
@@ -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
...
...
flask_admin/contrib/geoa/fields.py
View file @
26bb7798
...
@@ -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
...
...
flask_admin/contrib/geoa/form.py
View file @
26bb7798
...
@@ -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
)
flask_admin/contrib/geoa/typefmt.py
View file @
26bb7798
...
@@ -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
...
...
flask_admin/contrib/geoa/view.py
View file @
26bb7798
...
@@ -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
flask_admin/contrib/geoa/widgets.py
View file @
26bb7798
...
@@ -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
:
...
...
flask_admin/contrib/peewee/form.py
View file @
26bb7798
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
)
...
...
flask_admin/contrib/peewee/view.py
View file @
26bb7798
...
@@ -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
())
...
...
flask_admin/contrib/sqla/ajax.py
View file @
26bb7798
...
@@ -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
.
__
table
name__
.
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
:
...
...
flask_admin/contrib/sqla/fields.py
View file @
26bb7798
...
@@ -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
)
flask_admin/contrib/sqla/filters.py
View file @
26bb7798
...
@@ -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
]
...
...
flask_admin/contrib/sqla/form.py
View file @
26bb7798
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
)
...
...
flask_admin/contrib/sqla/view.py
View file @
26bb7798
...
@@ -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
)
...
...
flask_admin/model/base.py
View file @
26bb7798
...
@@ -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'
,))
...
...
flask_admin/model/typefmt.py
View file @
26bb7798
...
@@ -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
flask_admin/model/widgets.py
View file @
26bb7798
...
@@ -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'
]
=
'select
2
'
# 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'
]
=
'select
2
'
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'
]
=
'select
2
'
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
),))
...
...
flask_admin/static/admin/css/bootstrap2/admin.css
View file @
26bb7798
...
@@ -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
;
}
flask_admin/static/admin/css/bootstrap3/admin.css
View file @
26bb7798
...
@@ -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
;
}
}
...
...
flask_admin/static/admin/js/form.js
View file @
26bb7798
...
@@ -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 © <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 © <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,
...
...
flask_admin/templates/bootstrap2/admin/base.html
View file @
26bb7798
...
@@ -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 %}
...
...
flask_admin/templates/bootstrap3/admin/base.html
View file @
26bb7798
...
@@ -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"
>
...
...
flask_admin/tests/sqla/test_basic.py
View file @
26bb7798
...
@@ -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
)
...
...
setup.py
View file @
26bb7798
...
@@ -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.ini
View file @
26bb7798
[tox]
[tox]
envlist
=
envlist
=
py{2
6,27,33
,34,35,36}-WTForms{1,2}
py{2
7
,34,35,36}-WTForms{1,2}
flake8
flake8
docs-html
docs-html
skipsdist
=
true
skipsdist
=
true
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment