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
d3dddbf6
Commit
d3dddbf6
authored
Jul 12, 2014
by
Sergey Markelov
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'remotes/upstream/master'
parents
18267649
78a66cda
Changes
13
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
170 additions
and
83 deletions
+170
-83
.gitignore
.gitignore
+1
-0
quickstart.rst
doc/quickstart.rst
+1
-1
base.py
flask_admin/base.py
+6
-0
ajax.py
flask_admin/contrib/mongoengine/ajax.py
+1
-1
tools.py
flask_admin/contrib/pymongo/tools.py
+5
-3
filters.py
flask_admin/contrib/sqla/filters.py
+1
-1
view.py
flask_admin/contrib/sqla/view.py
+94
-72
actions.js
flask_admin/static/admin/js/actions.js
+1
-1
filters.js
flask_admin/static/admin/js/filters.js
+7
-2
base.html
flask_admin/templates/bootstrap3/admin/base.html
+1
-1
lib.html
flask_admin/templates/bootstrap3/admin/lib.html
+1
-1
create.html
flask_admin/templates/bootstrap3/admin/model/create.html
+2
-0
test_basic.py
flask_admin/tests/sqlamodel/test_basic.py
+49
-0
No files found.
.gitignore
View file @
d3dddbf6
...
...
@@ -18,6 +18,7 @@ venv
__pycache__
examples/sqla-inline/static
examples/file/files
examples/forms/files
.DS_Store
.idea/
*.sqlite
doc/quickstart.rst
View file @
d3dddbf6
...
...
@@ -274,7 +274,7 @@ administrative views.
File Admin
----------
Flask-Admin comes with another handy battery - file admin. It gives you ability to manage files on your server
Flask-Admin comes with another handy battery - file admin. It gives you
the
ability to manage files on your server
(upload, delete, rename, etc).
Here is simple example::
...
...
flask_admin/base.py
View file @
d3dddbf6
...
...
@@ -51,6 +51,10 @@ def expose_plugview(url='/'):
# Base views
def
_wrap_view
(
f
):
# Avoid wrapping view method twice
if
hasattr
(
f
,
'_wrapped'
):
return
f
@
wraps
(
f
)
def
inner
(
self
,
*
args
,
**
kwargs
):
# Store current admin view
...
...
@@ -63,6 +67,8 @@ def _wrap_view(f):
return
f
(
self
,
*
args
,
**
kwargs
)
inner
.
_wrapped
=
True
return
inner
...
...
flask_admin/contrib/mongoengine/ajax.py
View file @
d3dddbf6
...
...
@@ -134,7 +134,7 @@ def process_ajax_references(references, view):
field
=
getattr
(
model
,
name
,
None
)
if
not
field
:
raise
ValueError
(
'Invalid subdocument field
%
s.
%
s'
)
raise
ValueError
(
'Invalid subdocument field
%
s.
%
s'
%
(
model
,
name
)
)
handle_field
(
field
,
doc
,
make_name
(
base
,
name
))
...
...
flask_admin/contrib/pymongo/tools.py
View file @
d3dddbf6
import
re
def
parse_like_term
(
term
):
"""
Parse search term into (operation, term) tuple
...
...
@@ -6,8 +8,8 @@ def parse_like_term(term):
Search term
"""
if
term
.
startswith
(
'^'
):
return
'^
%
s'
%
term
[
1
]
return
'^
{}'
.
format
(
re
.
escape
(
term
[
1
:]))
elif
term
.
startswith
(
'='
):
return
'^
%
s$'
%
term
[
1
:]
return
'^
{}$'
.
format
(
re
.
escape
(
term
[
1
:]))
return
'
%
s'
%
term
return
re
.
escape
(
term
)
flask_admin/contrib/sqla/filters.py
View file @
d3dddbf6
...
...
@@ -100,7 +100,7 @@ class FilterConverter(filters.BaseFilterConverter):
return
None
@
filters
.
convert
(
'string'
,
'unicode'
,
'text'
,
'unicodetext'
)
@
filters
.
convert
(
'string'
,
'unicode'
,
'text'
,
'unicodetext'
,
'varchar'
)
def
conv_string
(
self
,
column
,
name
,
**
kwargs
):
return
[
f
(
column
,
name
,
**
kwargs
)
for
f
in
self
.
strings
]
...
...
flask_admin/contrib/sqla/view.py
View file @
d3dddbf6
...
...
@@ -262,10 +262,12 @@ class ModelView(BaseModelView):
self
.
session
=
session
self
.
_search_fields
=
None
self
.
_search_joins
=
dict
()
self
.
_search_joins
=
[]
self
.
_filter_joins
=
dict
()
self
.
_sortable_joins
=
dict
()
if
self
.
form_choices
is
None
:
self
.
form_choices
=
{}
...
...
@@ -293,6 +295,41 @@ class ModelView(BaseModelView):
return
model
.
_sa_class_manager
.
mapper
.
iterate_properties
def
_get_columns_for_field
(
self
,
field
):
if
(
not
field
or
not
hasattr
(
field
,
'property'
)
or
not
hasattr
(
field
.
property
,
'columns'
)
or
not
field
.
property
.
columns
):
raise
Exception
(
'Invalid field
%
s: does not contains any columns.'
%
field
)
return
field
.
property
.
columns
def
_get_field_with_path
(
self
,
name
):
join_tables
=
[]
if
isinstance
(
name
,
string_types
):
model
=
self
.
model
for
attribute
in
name
.
split
(
'.'
):
value
=
getattr
(
model
,
attribute
)
if
(
hasattr
(
value
,
'property'
)
and
hasattr
(
value
.
property
,
'direction'
)):
model
=
value
.
property
.
mapper
.
class_
table
=
model
.
__table__
if
self
.
_need_join
(
table
):
join_tables
.
append
(
table
)
attr
=
value
else
:
attr
=
name
return
join_tables
,
attr
def
_need_join
(
self
,
table
):
return
table
not
in
self
.
model
.
_sa_class_manager
.
mapper
.
tables
# Scaffolding
def
scaffold_pk
(
self
):
"""
...
...
@@ -370,25 +407,35 @@ class ModelView(BaseModelView):
return
columns
def
_get_columns_for_field
(
self
,
field
):
if
isinstance
(
field
,
string_types
):
attr
=
getattr
(
self
.
model
,
field
,
None
)
def
get_sortable_columns
(
self
):
"""
Returns a dictionary of the sortable columns. Key is a model
field name and value is sort column (for example - attribute).
if
field
is
None
:
raise
Exception
(
'Field
%
s was not found.'
%
field
)
If `column_sortable_list` is set, will use it. Otherwise, will call
`scaffold_sortable_columns` to get them from the model.
"""
self
.
_sortable_joins
=
dict
()
if
self
.
column_sortable_list
is
None
:
return
self
.
scaffold_sortable_columns
()
else
:
attr
=
field
result
=
dict
()
if
(
not
attr
or
not
hasattr
(
attr
,
'property'
)
or
not
hasattr
(
attr
.
property
,
'columns'
)
or
not
attr
.
property
.
columns
):
raise
Exception
(
'Invalid field
%
s: does not contains any columns.'
%
field
)
for
c
in
self
.
column_sortable_list
:
if
isinstance
(
c
,
tuple
):
join_tables
,
column
=
self
.
_get_field_with_path
(
c
[
1
])
return
attr
.
property
.
columns
result
[
c
[
0
]]
=
column
def
_need_join
(
self
,
table
):
return
table
not
in
self
.
model
.
_sa_class_manager
.
mapper
.
tables
if
join_tables
:
self
.
_sortable_joins
[
c
[
0
]]
=
join_tables
else
:
join_tables
,
column
=
self
.
_get_field_with_path
(
c
)
result
[
c
]
=
column
return
result
def
init_search
(
self
):
"""
...
...
@@ -400,10 +447,17 @@ class ModelView(BaseModelView):
"""
if
self
.
column_searchable_list
:
self
.
_search_fields
=
[]
self
.
_search_joins
=
dict
()
self
.
_search_joins
=
[]
joins
=
set
()
for
p
in
self
.
column_searchable_list
:
for
column
in
self
.
_get_columns_for_field
(
p
):
join_tables
,
attr
=
self
.
_get_field_with_path
(
p
)
if
not
attr
:
raise
Exception
(
'Failed to find field for search field:
%
s'
%
p
)
for
column
in
self
.
_get_columns_for_field
(
attr
):
column_type
=
type
(
column
.
type
)
.
__name__
if
not
self
.
is_text_column_type
(
column_type
):
...
...
@@ -412,9 +466,11 @@ class ModelView(BaseModelView):
self
.
_search_fields
.
append
(
column
)
# If it belongs to different table - add a join
if
self
.
_need_join
(
column
.
table
):
self
.
_search_joins
[
column
.
table
.
name
]
=
column
.
table
# Store joins, avoid duplicates
for
table
in
join_tables
:
if
table
.
name
not
in
joins
:
self
.
_search_joins
.
append
(
table
)
joins
.
add
(
table
.
name
)
return
bool
(
self
.
column_searchable_list
)
...
...
@@ -435,23 +491,7 @@ class ModelView(BaseModelView):
Return list of enabled filters
"""
join_tables
=
[]
if
isinstance
(
name
,
string_types
):
model
=
self
.
model
for
attribute
in
name
.
split
(
'.'
):
value
=
getattr
(
model
,
attribute
)
if
(
hasattr
(
value
,
'property'
)
and
hasattr
(
value
.
property
,
'direction'
)):
model
=
value
.
property
.
mapper
.
class_
table
=
model
.
__table__
if
self
.
_need_join
(
table
):
join_tables
.
append
(
table
)
attr
=
value
else
:
attr
=
name
join_tables
,
attr
=
self
.
_get_field_with_path
(
name
)
if
attr
is
None
:
raise
Exception
(
'Failed to find field for filter:
%
s'
%
name
)
...
...
@@ -616,7 +656,7 @@ class ModelView(BaseModelView):
"""
return
self
.
session
.
query
(
func
.
count
(
'*'
))
.
select_from
(
self
.
model
)
def
_order_by
(
self
,
query
,
joins
,
sort_field
,
sort_desc
):
def
_order_by
(
self
,
query
,
joins
,
sort_
joins
,
sort_
field
,
sort_desc
):
"""
Apply order_by to the query
...
...
@@ -630,33 +670,13 @@ class ModelView(BaseModelView):
Ascending or descending
"""
# TODO: Preprocessing for joins
# Try to handle it as a string
if
isinstance
(
sort_field
,
string_types
):
# Create automatic join against a table if column name
# contains dot.
if
'.'
in
sort_field
:
parts
=
sort_field
.
split
(
'.'
,
1
)
if
parts
[
0
]
not
in
joins
:
query
=
query
.
join
(
parts
[
0
])
joins
.
add
(
parts
[
0
])
elif
isinstance
(
sort_field
,
InstrumentedAttribute
):
# SQLAlchemy 0.8+ uses 'parent' as a name
mapper
=
getattr
(
sort_field
,
'parent'
,
None
)
if
mapper
is
None
:
# SQLAlchemy 0.7.x uses parententity
mapper
=
getattr
(
sort_field
,
'parententity'
,
None
)
if
mapper
is
not
None
:
table
=
mapper
.
tables
[
0
]
if
self
.
_need_join
(
table
)
and
table
.
name
not
in
joins
:
# Handle joins
if
sort_joins
:
for
table
in
sort_joins
:
if
table
.
name
not
in
joins
:
query
=
query
.
outerjoin
(
table
)
joins
.
add
(
table
.
name
)
elif
isinstance
(
sort_field
,
Column
):
pass
else
:
raise
TypeError
(
'Wrong argument type'
)
if
sort_field
is
not
None
:
if
sort_desc
:
...
...
@@ -672,10 +692,9 @@ class ModelView(BaseModelView):
if
order
is
not
None
:
field
,
direction
=
order
if
isinstance
(
field
,
string_types
):
field
=
getattr
(
self
.
model
,
field
)
join_tables
,
attr
=
self
.
_get_field_with_path
(
field
)
return
field
,
direction
return
join_tables
,
field
,
direction
return
None
...
...
@@ -707,11 +726,11 @@ class ModelView(BaseModelView):
if
self
.
_search_supported
and
search
:
# Apply search-related joins
if
self
.
_search_joins
:
for
jn
in
self
.
_search_joins
.
values
()
:
query
=
query
.
join
(
jn
)
count_query
=
count_query
.
join
(
jn
)
for
table
in
self
.
_search_joins
:
query
=
query
.
join
(
table
)
count_query
=
count_query
.
join
(
table
)
joins
=
set
(
self
.
_search_joins
.
keys
()
)
joins
.
add
(
table
.
name
)
# Apply terms
terms
=
search
.
split
(
' '
)
...
...
@@ -756,13 +775,16 @@ class ModelView(BaseModelView):
if
sort_column
is
not
None
:
if
sort_column
in
self
.
_sortable_columns
:
sort_field
=
self
.
_sortable_columns
[
sort_column
]
sort_joins
=
self
.
_sortable_joins
.
get
(
sort_column
)
query
,
joins
=
self
.
_order_by
(
query
,
joins
,
sort_field
,
sort_desc
)
query
,
joins
=
self
.
_order_by
(
query
,
joins
,
sort_
joins
,
sort_
field
,
sort_desc
)
else
:
order
=
self
.
_get_default_order
()
if
order
:
query
,
joins
=
self
.
_order_by
(
query
,
joins
,
order
[
0
],
order
[
1
])
sort_joins
,
sort_field
,
sort_desc
=
order
query
,
joins
=
self
.
_order_by
(
query
,
joins
,
sort_joins
,
sort_field
,
sort_desc
)
# Pagination
if
page
is
not
None
:
...
...
flask_admin/static/admin/js/actions.js
View file @
d3dddbf6
...
...
@@ -30,7 +30,7 @@ var AdminModelActions = function(actionErrorMessage, actionConfirmations) {
$
(
function
()
{
$
(
'.action-rowtoggle'
).
change
(
function
()
{
$
(
'input.action-checkbox'
).
attr
(
'checked'
,
this
.
checked
);
$
(
'input.action-checkbox'
).
prop
(
'checked'
,
this
.
checked
);
});
});
};
flask_admin/static/admin/js/filters.js
View file @
d3dddbf6
...
...
@@ -29,8 +29,13 @@ var AdminFilters = function(element, filtersElement, filterGroups) {
function
removeFilter
()
{
$
(
this
).
closest
(
'tr'
).
remove
();
$
(
'button'
,
$root
).
show
();
if
(
$
(
'.filters tr'
).
length
==
0
)
{
$
(
'button'
,
$root
).
hide
();
$
(
'a[class=btn]'
,
$root
).
hide
();
}
else
{
$
(
'button'
,
$root
).
show
();
}
return
false
;
}
...
...
flask_admin/templates/bootstrap3/admin/base.html
View file @
d3dddbf6
...
...
@@ -36,7 +36,7 @@
{% endblock %}
</div>
<!-- navbar content -->
<div
class=
"collapse navbar-collapse"
id=
"
#
admin-navbar-collapse"
>
<div
class=
"collapse navbar-collapse"
id=
"admin-navbar-collapse"
>
{% block main_menu %}
<ul
class=
"nav navbar-nav"
>
{{ layout.menu() }}
...
...
flask_admin/templates/bootstrap3/admin/lib.html
View file @
d3dddbf6
...
...
@@ -83,7 +83,7 @@
{%- endif %}
</label>
<div
class=
"
col-md-4
"
>
<div
class=
"
{{ kwargs.get('column_class', 'col-md-4') }}
"
>
{% set _dummy = kwargs.setdefault('class', 'form-control') %}
{{ field(**kwargs)|safe }}
</div>
...
...
flask_admin/templates/bootstrap3/admin/model/create.html
View file @
d3dddbf6
...
...
@@ -13,6 +13,7 @@
{% endblock %}
{% block body %}
{% block navlinks %}
<ul
class=
"nav nav-tabs"
>
<li>
<a
href=
"{{ return_url }}"
>
{{ _gettext('List') }}
</a>
...
...
@@ -21,6 +22,7 @@
<a
href=
"javascript:void(0)"
>
{{ _gettext('Create') }}
</a>
</li>
</ul>
{% endblock %}
{% call lib.form_tag(form) %}
{{ lib.render_form_fields(form, form_opts=form_opts) }}
...
...
flask_admin/tests/sqlamodel/test_basic.py
View file @
d3dddbf6
...
...
@@ -38,6 +38,9 @@ def create_models(db):
bool_field
=
db
.
Column
(
db
.
Boolean
)
enum_field
=
db
.
Column
(
db
.
Enum
(
'model1_v1'
,
'model1_v1'
),
nullable
=
True
)
def
__unicode__
(
self
):
return
self
.
test1
def
__str__
(
self
):
return
self
.
test1
...
...
@@ -220,6 +223,29 @@ def test_column_searchable_list():
ok_
(
'model2'
not
in
data
)
def
test_complex_searchable_list
():
app
,
db
,
admin
=
setup
()
Model1
,
Model2
=
create_models
(
db
)
view
=
CustomModelView
(
Model2
,
db
.
session
,
column_searchable_list
=
[
'model1.test1'
])
admin
.
add_view
(
view
)
m1
=
Model1
(
'model1'
)
db
.
session
.
add
(
m1
)
db
.
session
.
add
(
Model2
(
'model2'
,
model1
=
m1
))
db
.
session
.
add
(
Model2
(
'model3'
))
db
.
session
.
commit
()
client
=
app
.
test_client
()
rv
=
client
.
get
(
'/admin/model2/?search=model1'
)
data
=
rv
.
data
.
decode
(
'utf-8'
)
ok_
(
'model1'
in
data
)
ok_
(
'model3'
not
in
data
)
def
test_column_filters
():
app
,
db
,
admin
=
setup
()
...
...
@@ -634,6 +660,29 @@ def test_default_sort():
eq_
(
data
[
2
]
.
test1
,
'c'
)
def
test_default_complex_sort
():
app
,
db
,
admin
=
setup
()
M1
,
M2
=
create_models
(
db
)
m1
=
M1
(
'b'
)
db
.
session
.
add
(
m1
)
db
.
session
.
add
(
M2
(
'c'
,
model1
=
m1
))
m2
=
M1
(
'a'
)
db
.
session
.
add
(
m2
)
db
.
session
.
add
(
M2
(
'c'
,
model1
=
m2
))
db
.
session
.
commit
()
view
=
CustomModelView
(
M2
,
db
.
session
,
column_default_sort
=
'model1.test1'
)
admin
.
add_view
(
view
)
_
,
data
=
view
.
get_list
(
0
,
None
,
None
,
None
,
None
)
eq_
(
len
(
data
),
2
)
eq_
(
data
[
0
]
.
model1
.
test1
,
'a'
)
eq_
(
data
[
1
]
.
model1
.
test1
,
'b'
)
def
test_extra_fields
():
app
,
db
,
admin
=
setup
()
...
...
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