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
73ed6524
Commit
73ed6524
authored
Mar 27, 2012
by
Serge S. Koval
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Started working on the filters.
parent
0edfb4a9
Changes
13
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
609 additions
and
64 deletions
+609
-64
TODO.txt
TODO.txt
+3
-2
simple.py
examples/sqla/simple.py
+4
-1
__init__.py
flask_adminex/ext/sqlamodel/__init__.py
+1
-0
filters.py
flask_adminex/ext/sqlamodel/filters.py
+102
-0
tools.py
flask_adminex/ext/sqlamodel/tools.py
+9
-0
view.py
flask_adminex/ext/sqlamodel/view.py
+73
-28
__init__.py
flask_adminex/model/__init__.py
+1
-0
base.py
flask_adminex/model/base.py
+150
-30
filters.py
flask_adminex/model/filters.py
+70
-0
admin.css
flask_adminex/static/css/admin.css
+17
-1
filters.js
flask_adminex/static/js/filters.js
+108
-0
form.js
flask_adminex/static/js/form.js
+20
-2
list.html
flask_adminex/templates/admin/model/list.html
+51
-0
No files found.
TODO.txt
View file @
73ed6524
...
@@ -3,9 +3,9 @@
...
@@ -3,9 +3,9 @@
- Calendar - add validation for time without seconds (automatically add seconds)
- Calendar - add validation for time without seconds (automatically add seconds)
- Model Admin
- Model Admin
- Ability to sort by fields that are not visible?
- Ability to sort by fields that are not visible?
- Exclude for list columns
- Exclude for form fields
- List display callables
- List display callables
- Search
- Rename init_search
- Built-in filtering support
- Built-in filtering support
- Configurable operations (=, >, <, etc)
- Configurable operations (=, >, <, etc)
- Callable operations
- Callable operations
...
@@ -20,5 +20,6 @@
...
@@ -20,5 +20,6 @@
- Header title
- Header title
- Mass-delete functionality
- Mass-delete functionality
- File size restriction
- File size restriction
- Localization
- Unit tests
- Unit tests
- Documentation
- Documentation
examples/sqla/simple.py
View file @
73ed6524
...
@@ -3,6 +3,7 @@ from flaskext.sqlalchemy import SQLAlchemy
...
@@ -3,6 +3,7 @@ from flaskext.sqlalchemy import SQLAlchemy
from
flask.ext
import
adminex
,
wtf
from
flask.ext
import
adminex
,
wtf
from
flask.ext.adminex.ext
import
sqlamodel
from
flask.ext.adminex.ext
import
sqlamodel
from
flask.ext.adminex.ext.sqlamodel
import
filters
# Create application
# Create application
app
=
Flask
(
__name__
)
app
=
Flask
(
__name__
)
...
@@ -61,6 +62,8 @@ class PostAdmin(sqlamodel.ModelView):
...
@@ -61,6 +62,8 @@ class PostAdmin(sqlamodel.ModelView):
searchable_columns
=
(
'title'
,
User
.
username
)
searchable_columns
=
(
'title'
,
User
.
username
)
column_filters
=
(
User
.
username
,
'title'
,
'date'
,
filters
.
FilterLike
(
Post
.
title
,
'Fixed Title'
,
options
=
((
'test1'
,
'Test 1'
),
(
'test2'
,
'Test 2'
))))
# Pass arguments to WTForms. In this case, change label for text field to
# Pass arguments to WTForms. In this case, change label for text field to
# be 'Big Text' and add required() validator.
# be 'Big Text' and add required() validator.
form_args
=
dict
(
form_args
=
dict
(
...
@@ -84,4 +87,4 @@ if __name__ == '__main__':
...
@@ -84,4 +87,4 @@ if __name__ == '__main__':
# Start app
# Start app
app
.
debug
=
True
app
.
debug
=
True
app
.
run
()
app
.
run
(
'0.0.0.0'
,
8000
)
flask_adminex/ext/sqlamodel/__init__.py
0 → 100644
View file @
73ed6524
from
.view
import
ModelView
flask_adminex/ext/sqlamodel/filters.py
0 → 100644
View file @
73ed6524
from
flask.ext.adminex.model
import
filters
from
flask.ext.adminex.ext.sqlamodel
import
tools
class
BaseSQLAFilter
(
filters
.
BaseFilter
):
def
__init__
(
self
,
column
,
name
,
options
=
None
,
data_type
=
None
):
super
(
BaseSQLAFilter
,
self
)
.
__init__
(
name
,
options
,
data_type
)
self
.
column
=
column
# Common filters
class
FilterEqual
(
BaseSQLAFilter
):
def
apply
(
self
,
query
,
value
):
return
query
.
filter
(
self
.
column
==
value
)
def
__unicode__
(
self
):
return
'
%
s equals'
%
self
.
name
class
FilterNotEqual
(
BaseSQLAFilter
):
def
apply
(
self
,
query
,
value
):
return
query
.
filter
(
self
.
column
!=
value
)
def
__unicode__
(
self
):
return
'
%
s not equal'
%
self
.
name
class
FilterLike
(
BaseSQLAFilter
):
def
apply
(
self
,
query
,
value
):
stmt
=
tools
.
parse_like_term
(
value
)
return
query
.
filter
(
self
.
column
.
ilike
(
stmt
))
def
__unicode__
(
self
):
return
'
%
s like'
%
self
.
name
class
FilterNotLike
(
BaseSQLAFilter
):
def
apply
(
self
,
query
,
value
):
stmt
=
tools
.
parse_like_term
(
value
)
return
query
.
filter
(
~
self
.
column
.
ilike
(
stmt
))
def
__unicode__
(
self
):
return
'
%
s not like'
%
self
.
name
class
FilterGreater
(
BaseSQLAFilter
):
def
apply
(
self
,
query
,
value
):
return
query
.
filter
(
self
.
column
>
value
)
def
__unicode__
(
self
):
return
'
%
s greater than'
%
self
.
name
class
FilterSmaller
(
BaseSQLAFilter
):
def
apply
(
self
,
query
,
value
):
return
query
.
filter
(
self
.
column
<
value
)
def
__unicode__
(
self
):
return
'
%
s smaller than'
%
self
.
name
# Customized type filters
class
BooleanEqualFilter
(
FilterEqual
,
filters
.
BaseBooleanFilter
):
pass
class
BooleanNotEqualFilter
(
FilterNotEqual
,
filters
.
BaseBooleanFilter
):
pass
# Base SQLA filter field converter
class
FilterConverter
(
filters
.
BaseFilterConverter
):
strings
=
(
FilterEqual
,
FilterNotEqual
,
FilterLike
,
FilterNotLike
)
numeric
=
(
FilterEqual
,
FilterNotEqual
,
FilterGreater
,
FilterSmaller
)
def
convert
(
self
,
type_name
,
column
,
name
):
if
type_name
in
self
.
converters
:
return
self
.
converters
[
type_name
](
column
,
name
)
return
None
@
filters
.
convert
(
'String'
,
'Unicode'
,
'Text'
,
'UnicodeText'
)
def
conv_string
(
self
,
column
,
name
):
return
[
f
(
column
,
name
)
for
f
in
self
.
strings
]
@
filters
.
convert
(
'Boolean'
)
def
conv_bool
(
self
,
column
,
name
):
return
[
BooleanEqualFilter
(
column
,
name
),
BooleanNotEqualFilter
(
column
,
name
)]
@
filters
.
convert
(
'Integer'
,
'SmallInteger'
,
'Numeric'
,
'Float'
)
def
conv_int
(
self
,
column
,
name
):
return
[
f
(
column
,
name
)
for
f
in
self
.
numeric
]
@
filters
.
convert
(
'Date'
)
def
conv_date
(
self
,
column
,
name
):
return
[
f
(
column
,
name
,
data_type
=
'datepicker'
)
for
f
in
self
.
numeric
]
@
filters
.
convert
(
'DateTime'
)
def
conv_datetime
(
self
,
column
,
name
):
return
[
f
(
column
,
name
,
data_type
=
'datetimepicker'
)
for
f
in
self
.
numeric
]
flask_adminex/ext/sqlamodel/tools.py
0 → 100644
View file @
73ed6524
def
parse_like_term
(
term
):
if
term
.
startswith
(
'^'
):
stmt
=
'
%
s
%%
'
%
term
[
1
:]
elif
term
.
startswith
(
'='
):
stmt
=
term
[
1
:]
else
:
stmt
=
'
%%%
s
%%
'
%
term
return
stmt
flask_adminex/ext/sqlamodel.py
→
flask_adminex/ext/sqlamodel
/view
.py
View file @
73ed6524
...
@@ -12,6 +12,7 @@ from flask import flash
...
@@ -12,6 +12,7 @@ from flask import flash
from
flask.ext.adminex
import
form
from
flask.ext.adminex
import
form
from
flask.ext.adminex.model
import
BaseModelView
from
flask.ext.adminex.model
import
BaseModelView
from
flask.ext.adminex.ext.sqlamodel
import
filters
,
tools
class
Unique
(
object
):
class
Unique
(
object
):
...
@@ -220,6 +221,11 @@ class ModelView(BaseModelView):
...
@@ -220,6 +221,11 @@ class ModelView(BaseModelView):
For example, if you entered *=ZZZ*, *ILIKE 'ZZZ'* statement will be used.
For example, if you entered *=ZZZ*, *ILIKE 'ZZZ'* statement will be used.
"""
"""
filter_converter
=
filters
.
FilterConverter
()
"""
TBD:
"""
def
__init__
(
self
,
model
,
session
,
def
__init__
(
self
,
model
,
session
,
name
=
None
,
category
=
None
,
endpoint
=
None
,
url
=
None
):
name
=
None
,
category
=
None
,
endpoint
=
None
,
url
=
None
):
"""
"""
...
@@ -241,8 +247,9 @@ class ModelView(BaseModelView):
...
@@ -241,8 +247,9 @@ class ModelView(BaseModelView):
self
.
session
=
session
self
.
session
=
session
self
.
_search_fields
=
None
self
.
_search_fields
=
None
self
.
_search_joins
=
None
self
.
_search_joins_names
=
set
()
self
.
_search_joins_names
=
None
self
.
_filter_joins_names
=
set
()
super
(
ModelView
,
self
)
.
__init__
(
model
,
name
,
category
,
endpoint
,
url
)
super
(
ModelView
,
self
)
.
__init__
(
model
,
name
,
category
,
endpoint
,
url
)
...
@@ -312,6 +319,23 @@ class ModelView(BaseModelView):
...
@@ -312,6 +319,23 @@ class ModelView(BaseModelView):
return
columns
return
columns
def
_get_columns_for_field
(
self
,
field
):
if
isinstance
(
field
,
basestring
):
attr
=
getattr
(
self
.
model
,
field
,
None
)
if
field
is
None
:
raise
Exception
(
'Field
%
s was not found.'
%
field
)
else
:
attr
=
field
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
)
return
attr
.
property
.
columns
def
init_search
(
self
):
def
init_search
(
self
):
"""
"""
Initialize search. Returns `True` if search is supported for this
Initialize search. Returns `True` if search is supported for this
...
@@ -322,23 +346,10 @@ class ModelView(BaseModelView):
...
@@ -322,23 +346,10 @@ class ModelView(BaseModelView):
"""
"""
if
self
.
searchable_columns
:
if
self
.
searchable_columns
:
self
.
_search_fields
=
[]
self
.
_search_fields
=
[]
self
.
_search_joins
=
[]
self
.
_search_joins_names
=
set
()
self
.
_search_joins_names
=
set
()
for
p
in
self
.
searchable_columns
:
for
p
in
self
.
searchable_columns
:
# If item is a stirng, resolve it as an attribute
for
column
in
self
.
_get_columns_for_field
(
p
):
if
isinstance
(
p
,
basestring
):
attr
=
getattr
(
self
.
model
,
p
,
None
)
else
:
attr
=
p
# Only column searches are supported
if
(
not
attr
or
not
hasattr
(
attr
,
'property'
)
or
not
hasattr
(
attr
.
property
,
'columns'
)):
raise
Exception
(
'Invalid searchable column "
%
s"'
%
p
)
for
column
in
attr
.
property
.
columns
:
column_type
=
type
(
column
.
type
)
.
__name__
column_type
=
type
(
column
.
type
)
.
__name__
if
not
self
.
is_text_column_type
(
column_type
):
if
not
self
.
is_text_column_type
(
column_type
):
...
@@ -349,7 +360,6 @@ class ModelView(BaseModelView):
...
@@ -349,7 +360,6 @@ class ModelView(BaseModelView):
# If it belongs to different table - add a join
# If it belongs to different table - add a join
if
column
.
table
!=
self
.
model
.
__table__
:
if
column
.
table
!=
self
.
model
.
__table__
:
self
.
_search_joins
.
append
(
column
.
table
)
self
.
_search_joins_names
.
add
(
column
.
table
.
name
)
self
.
_search_joins_names
.
add
(
column
.
table
.
name
)
return
bool
(
self
.
searchable_columns
)
return
bool
(
self
.
searchable_columns
)
...
@@ -363,6 +373,31 @@ class ModelView(BaseModelView):
...
@@ -363,6 +373,31 @@ class ModelView(BaseModelView):
return
(
name
==
'String'
or
name
==
'Unicode'
or
return
(
name
==
'String'
or
name
==
'Unicode'
or
name
==
'Text'
or
name
==
'UnicodeText'
)
name
==
'Text'
or
name
==
'UnicodeText'
)
def
scaffold_filters
(
self
,
name
):
columns
=
self
.
_get_columns_for_field
(
name
)
if
len
(
columns
)
>
1
:
raise
Exception
(
'Can not filter more than on one column for
%
s'
%
name
)
column
=
columns
[
0
]
if
not
isinstance
(
name
,
basestring
):
visible_name
=
self
.
get_column_name
(
name
.
property
.
key
)
else
:
visible_name
=
self
.
get_column_name
(
name
)
type_name
=
type
(
column
.
type
)
.
__name__
flt
=
self
.
filter_converter
.
convert
(
type_name
,
column
,
visible_name
)
if
flt
:
# If there's relation to other table, do it
if
column
.
table
!=
self
.
model
.
__table__
:
self
.
_filter_joins_names
.
add
(
column
.
table
.
name
)
return
flt
def
scaffold_form
(
self
):
def
scaffold_form
(
self
):
"""
"""
Create form from the model.
Create form from the model.
...
@@ -395,7 +430,7 @@ class ModelView(BaseModelView):
...
@@ -395,7 +430,7 @@ class ModelView(BaseModelView):
return
joined
return
joined
# Database-related API
# Database-related API
def
get_list
(
self
,
page
,
sort_column
,
sort_desc
,
search
,
execute
=
True
):
def
get_list
(
self
,
page
,
sort_column
,
sort_desc
,
search
,
filters
,
execute
=
True
):
"""
"""
Return models from the database.
Return models from the database.
...
@@ -409,6 +444,8 @@ class ModelView(BaseModelView):
...
@@ -409,6 +444,8 @@ class ModelView(BaseModelView):
Search query
Search query
`execute`
`execute`
Execute query immediately? Default is `True`
Execute query immediately? Default is `True`
`filters`
List of filter tuples
"""
"""
# Will contain names of joined tables to avoid duplicate joins
# Will contain names of joined tables to avoid duplicate joins
...
@@ -416,11 +453,11 @@ class ModelView(BaseModelView):
...
@@ -416,11 +453,11 @@ class ModelView(BaseModelView):
query
=
self
.
session
.
query
(
self
.
model
)
query
=
self
.
session
.
query
(
self
.
model
)
# Apply search
before counting results
# Apply search
criteria
if
self
.
_search_supported
and
search
:
if
self
.
_search_supported
and
search
:
# Apply search-related joins
# Apply search-related joins
if
self
.
_search_joins
:
if
self
.
_search_joins
_names
:
query
=
query
.
join
(
*
self
.
_search_joins
)
query
=
query
.
join
(
*
self
.
_search_joins
_names
)
joins
|=
self
.
_search_joins_names
joins
|=
self
.
_search_joins_names
# Apply terms
# Apply terms
...
@@ -430,16 +467,24 @@ class ModelView(BaseModelView):
...
@@ -430,16 +467,24 @@ class ModelView(BaseModelView):
if
not
term
:
if
not
term
:
continue
continue
if
term
.
startswith
(
'^'
):
stmt
=
tools
.
parse_like_term
(
term
)
stmt
=
'
%
s
%%
'
%
term
[
1
:]
elif
term
.
startswith
(
'='
):
stmt
=
term
[
1
:]
else
:
stmt
=
'
%%%
s
%%
'
%
term
filter_stmt
=
[
c
.
ilike
(
stmt
)
for
c
in
self
.
_search_fields
]
filter_stmt
=
[
c
.
ilike
(
stmt
)
for
c
in
self
.
_search_fields
]
query
=
query
.
filter
(
or_
(
*
filter_stmt
))
query
=
query
.
filter
(
or_
(
*
filter_stmt
))
# Apply filters
if
self
.
_filters
:
# Apply search-related joins
if
self
.
_filter_joins_names
:
new_joins
=
self
.
_filter_joins_names
-
joins
if
new_joins
:
query
=
query
.
join
(
*
new_joins
)
joins
|=
self
.
_search_joins_names
# Apply filters
for
flt
,
value
in
filters
:
query
=
self
.
_filters
[
flt
]
.
apply
(
query
,
value
)
# Calculate number of rows
# Calculate number of rows
count
=
query
.
count
()
count
=
query
.
count
()
...
...
flask_adminex/model/__init__.py
0 → 100644
View file @
73ed6524
from
.base
import
BaseModelView
flask_adminex/model.py
→
flask_adminex/model
/base
.py
View file @
73ed6524
from
itertools
import
count
from
flask
import
request
,
url_for
,
redirect
,
flash
from
flask
import
request
,
url_for
,
redirect
,
flash
from
.base
import
BaseView
,
expose
from
flask.ext.adminex.base
import
BaseView
,
expose
from
flask.ext.adminex.model
import
filters
class
BaseModelView
(
BaseView
):
class
BaseModelView
(
BaseView
):
...
@@ -98,8 +101,8 @@ class BaseModelView(BaseView):
...
@@ -98,8 +101,8 @@ class BaseModelView(BaseView):
searchable_columns
=
None
searchable_columns
=
None
"""
"""
Collection of the searchable columns. It is assumed that only
Collection of the searchable columns. It is assumed that only
text-only fields are searchable, but it is up for a model
implementation
text-only fields are searchable, but it is up for a model
to make decision.
implementation
to make decision.
For example::
For example::
...
@@ -107,6 +110,13 @@ class BaseModelView(BaseView):
...
@@ -107,6 +110,13 @@ class BaseModelView(BaseView):
searchable_columns = ('name', 'email')
searchable_columns = ('name', 'email')
"""
"""
column_filters
=
None
"""
Collection of the column filters.
TBD: Doc
"""
form_columns
=
None
form_columns
=
None
"""
"""
Collection of the model field names for the form. If set to `None` will
Collection of the model field names for the form. If set to `None` will
...
@@ -186,14 +196,29 @@ class BaseModelView(BaseView):
...
@@ -186,14 +196,29 @@ class BaseModelView(BaseView):
"""
"""
Refresh various cached variables.
Refresh various cached variables.
"""
"""
# List view
self
.
_list_columns
=
self
.
get_list_columns
()
self
.
_list_columns
=
self
.
get_list_columns
()
self
.
_sortable_columns
=
self
.
get_sortable_columns
()
self
.
_sortable_columns
=
self
.
get_sortable_columns
()
# Forms
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
()
# Search
self
.
_search_supported
=
self
.
init_search
()
self
.
_search_supported
=
self
.
init_search
()
# Filters
self
.
_filters
=
self
.
get_filters
()
if
self
.
_filters
:
self
.
_filter_names
=
[
unicode
(
n
)
for
n
in
self
.
_filters
]
self
.
_filter_types
=
dict
((
i
,
f
.
data_type
)
for
i
,
f
in
enumerate
(
self
.
_filters
)
if
f
.
data_type
)
else
:
self
.
_filter_names
=
None
self
.
_filter_types
=
None
# Public API
# Public API
def
scaffold_list_columns
(
self
):
def
scaffold_list_columns
(
self
):
"""
"""
...
@@ -207,26 +232,30 @@ class BaseModelView(BaseView):
...
@@ -207,26 +232,30 @@ class BaseModelView(BaseView):
"""
"""
raise
NotImplemented
(
'Please implement scaffold_list_columns method'
)
raise
NotImplemented
(
'Please implement scaffold_list_columns method'
)
def
get_column_name
(
self
,
field
):
"""
Return human-readable column name.
`field`
Model field name.
"""
if
self
.
rename_columns
and
field
in
self
.
rename_columns
:
return
self
.
rename_columns
[
field
]
else
:
return
self
.
prettify_name
(
field
)
def
get_list_columns
(
self
):
def
get_list_columns
(
self
):
"""
"""
Returns list of the model field names. If `list_columns` was
Returns list of the model field names. If `list_columns` was
set, returns it. Otherwise calls `scaffold_list_columns`
set, returns it. Otherwise calls `scaffold_list_columns`
to generate list from the model.
to generate list from the model.
"""
"""
result
=
[]
if
self
.
list_columns
is
None
:
if
self
.
list_columns
is
None
:
columns
=
self
.
scaffold_list_columns
()
columns
=
self
.
scaffold_list_columns
()
else
:
else
:
columns
=
self
.
list_columns
columns
=
self
.
list_columns
for
c
in
columns
:
return
[(
c
,
self
.
get_column_name
(
c
))
for
c
in
columns
]
if
self
.
rename_columns
and
c
in
self
.
rename_columns
:
result
.
append
((
c
,
self
.
rename_columns
[
c
]))
else
:
result
.
append
((
c
,
self
.
prettify_name
(
c
)))
return
result
def
scaffold_sortable_columns
(
self
):
def
scaffold_sortable_columns
(
self
):
"""
"""
...
@@ -266,10 +295,51 @@ class BaseModelView(BaseView):
...
@@ -266,10 +295,51 @@ class BaseModelView(BaseView):
"""
"""
return
False
return
False
def
scaffold_filter
(
self
,
name
):
"""
Generate filter object for the given name
`name`
Name of the field
"""
return
None
def
is_valid_filter
(
self
,
filter
):
"""
Verify that provided filter object is valid.
`filter`
Filter object to verify.
"""
return
isinstance
(
filter
,
filters
.
BaseFilter
)
def
get_filters
(
self
):
"""
Return list of filter objects.
"""
if
self
.
column_filters
:
collection
=
[]
for
n
in
self
.
column_filters
:
if
not
self
.
is_valid_filter
(
n
):
flt
=
self
.
scaffold_filters
(
n
)
if
flt
:
collection
.
extend
(
flt
)
else
:
raise
Exception
(
'Unsupported filter type
%
s'
%
n
)
else
:
collection
.
append
(
n
)
print
collection
return
collection
else
:
return
None
def
scaffold_form
(
self
):
def
scaffold_form
(
self
):
"""
"""
Create `form.BaseForm` inherited class from the model. Must be
implemented in
Create `form.BaseForm` inherited class from the model. Must be
the child class.
implemented in
the child class.
"""
"""
raise
NotImplemented
(
'Please implement scaffold_form method'
)
raise
NotImplemented
(
'Please implement scaffold_form method'
)
...
@@ -325,7 +395,7 @@ class BaseModelView(BaseView):
...
@@ -325,7 +395,7 @@ class BaseModelView(BaseView):
return
self
.
_list_columns
[
idx
]
return
self
.
_list_columns
[
idx
]
# Database-related API
# Database-related API
def
get_list
(
self
,
page
,
sort_field
,
sort_desc
,
search
):
def
get_list
(
self
,
page
,
sort_field
,
sort_desc
,
search
,
filters
):
"""
"""
Return list of models from the data source with applied pagination
Return list of models from the data source with applied pagination
and sorting.
and sorting.
...
@@ -340,6 +410,9 @@ class BaseModelView(BaseView):
...
@@ -340,6 +410,9 @@ class BaseModelView(BaseView):
If set to True, sorting is in descending order.
If set to True, sorting is in descending order.
`search`
`search`
Search query
Search query
`filters`
List of filter tuples. First value in a tuple is a search
index, second value is a search value.
"""
"""
raise
NotImplemented
(
'Please implement get_list method'
)
raise
NotImplemented
(
'Please implement get_list method'
)
...
@@ -418,9 +491,28 @@ class BaseModelView(BaseView):
...
@@ -418,9 +491,28 @@ class BaseModelView(BaseView):
sort_desc
=
request
.
args
.
get
(
'desc'
,
None
,
type
=
int
)
sort_desc
=
request
.
args
.
get
(
'desc'
,
None
,
type
=
int
)
search
=
request
.
args
.
get
(
'search'
,
None
)
search
=
request
.
args
.
get
(
'search'
,
None
)
return
page
,
sort
,
sort_desc
,
search
# Gather filters
if
self
.
_filters
:
filters
=
[]
for
n
in
count
():
param
=
'flt
%
d'
%
n
if
param
not
in
request
.
args
:
break
def
_get_url
(
self
,
view
=
None
,
page
=
None
,
sort
=
None
,
sort_desc
=
None
,
search
=
None
):
idx
=
request
.
args
.
get
(
param
,
None
,
type
=
int
)
value
=
request
.
args
.
get
(
param
+
'v'
,
None
)
if
idx
>=
0
and
idx
<
len
(
self
.
_filters
):
if
self
.
_filters
[
idx
]
.
validate
(
value
):
filters
.
append
((
idx
,
value
))
else
:
filters
=
None
return
page
,
sort
,
sort_desc
,
search
,
filters
def
_get_url
(
self
,
view
=
None
,
page
=
None
,
sort
=
None
,
sort_desc
=
None
,
search
=
None
,
filters
=
None
):
"""
"""
Generate page URL with current page, sort column and
Generate page URL with current page, sort column and
other parameters.
other parameters.
...
@@ -435,6 +527,8 @@ class BaseModelView(BaseView):
...
@@ -435,6 +527,8 @@ class BaseModelView(BaseView):
Use descending sorting order
Use descending sorting order
`search`
`search`
Search query
Search query
`filters`
List of active filters
"""
"""
if
not
search
:
if
not
search
:
search
=
None
search
=
None
...
@@ -442,11 +536,16 @@ class BaseModelView(BaseView):
...
@@ -442,11 +536,16 @@ class BaseModelView(BaseView):
if
not
page
:
if
not
page
:
page
=
None
page
=
None
return
url_for
(
view
,
kwargs
=
dict
(
page
=
page
,
sort
=
sort
,
desc
=
sort_desc
,
search
=
search
)
page
=
page
,
sort
=
sort
,
if
filters
:
desc
=
sort_desc
,
for
i
,
flt
in
enumerate
(
filters
):
search
=
search
)
base
=
'flt
%
d'
%
i
kwargs
[
base
]
=
flt
[
0
]
kwargs
[
base
+
'v'
]
=
flt
[
1
]
return
url_for
(
view
,
**
kwargs
)
# Views
# Views
@
expose
(
'/'
)
@
expose
(
'/'
)
...
@@ -455,7 +554,7 @@ class BaseModelView(BaseView):
...
@@ -455,7 +554,7 @@ class BaseModelView(BaseView):
List view
List view
"""
"""
# Grab parameters from URL
# Grab parameters from URL
page
,
sort_idx
,
sort_desc
,
search
=
self
.
_get_extra_args
()
page
,
sort_idx
,
sort_desc
,
search
,
filters
=
self
.
_get_extra_args
()
# Map column index to column name
# Map column index to column name
sort_column
=
self
.
_get_column_by_idx
(
sort_idx
)
sort_column
=
self
.
_get_column_by_idx
(
sort_idx
)
...
@@ -463,20 +562,34 @@ class BaseModelView(BaseView):
...
@@ -463,20 +562,34 @@ class BaseModelView(BaseView):
sort_column
=
sort_column
[
0
]
sort_column
=
sort_column
[
0
]
# Get count and data
# Get count and data
count
,
data
=
self
.
get_list
(
page
,
sort_column
,
sort_desc
,
search
)
count
,
data
=
self
.
get_list
(
page
,
sort_column
,
sort_desc
,
search
,
filters
)
# Calculate number of pages
# Calculate number of pages
num_pages
=
count
/
self
.
page_size
num_pages
=
count
/
self
.
page_size
if
count
%
self
.
page_size
!=
0
:
if
count
%
self
.
page_size
!=
0
:
num_pages
+=
1
num_pages
+=
1
# Pregenerate filters
if
self
.
_filters
:
filters_data
=
dict
()
for
idx
,
f
in
enumerate
(
self
.
_filters
):
flt_data
=
f
.
get_options
(
self
)
if
flt_data
:
filters_data
[
idx
]
=
flt_data
else
:
filters_data
=
None
# Various URL generation helpers
# Various URL generation helpers
def
pager_url
(
p
):
def
pager_url
(
p
):
# Do not add page number if it is first page
# Do not add page number if it is first page
if
p
==
0
:
if
p
==
0
:
p
=
None
p
=
None
return
self
.
_get_url
(
'.index_view'
,
p
,
sort_idx
,
sort_desc
,
search
)
return
self
.
_get_url
(
'.index_view'
,
p
,
sort_idx
,
sort_desc
,
search
,
filters
)
def
sort_url
(
column
,
invert
=
False
):
def
sort_url
(
column
,
invert
=
False
):
desc
=
None
desc
=
None
...
@@ -484,7 +597,8 @@ class BaseModelView(BaseView):
...
@@ -484,7 +597,8 @@ class BaseModelView(BaseView):
if
invert
and
not
sort_desc
:
if
invert
and
not
sort_desc
:
desc
=
1
desc
=
1
return
self
.
_get_url
(
'.index_view'
,
page
,
column
,
desc
,
search
)
return
self
.
_get_url
(
'.index_view'
,
page
,
column
,
desc
,
search
,
filters
)
def
get_value
(
obj
,
field
):
def
get_value
(
obj
,
field
):
return
getattr
(
obj
,
field
,
None
)
return
getattr
(
obj
,
field
,
None
)
...
@@ -495,12 +609,14 @@ class BaseModelView(BaseView):
...
@@ -495,12 +609,14 @@ class BaseModelView(BaseView):
list_columns
=
self
.
_list_columns
,
list_columns
=
self
.
_list_columns
,
sortable_columns
=
self
.
_sortable_columns
,
sortable_columns
=
self
.
_sortable_columns
,
# Stuff
# Stuff
enumerate
=
enumerate
,
get_value
=
get_value
,
get_value
=
get_value
,
return_url
=
self
.
_get_url
(
'.index_view'
,
return_url
=
self
.
_get_url
(
'.index_view'
,
page
,
page
,
sort_idx
,
sort_idx
,
sort_desc
,
sort_desc
,
search
),
search
,
filters
),
# Pagination
# Pagination
pager_url
=
pager_url
,
pager_url
=
pager_url
,
num_pages
=
num_pages
,
num_pages
=
num_pages
,
...
@@ -514,9 +630,13 @@ class BaseModelView(BaseView):
...
@@ -514,9 +630,13 @@ class BaseModelView(BaseView):
clear_search_url
=
self
.
_get_url
(
'.index_view'
,
clear_search_url
=
self
.
_get_url
(
'.index_view'
,
None
,
None
,
sort_idx
,
sort_idx
,
sort_desc
,
sort_desc
),
None
),
search
=
search
,
search
=
search
# Filters
filter_names
=
self
.
_filter_names
,
filter_types
=
self
.
_filter_types
,
filter_data
=
filters_data
,
active_filters
=
filters
)
)
@
expose
(
'/new/'
,
methods
=
(
'GET'
,
'POST'
))
@
expose
(
'/new/'
,
methods
=
(
'GET'
,
'POST'
))
...
...
flask_adminex/model/filters.py
0 → 100644
View file @
73ed6524
class
BaseFilter
(
object
):
def
__init__
(
self
,
name
,
options
=
None
,
data_type
=
None
):
self
.
name
=
name
self
.
options
=
options
self
.
data_type
=
data_type
def
get_options
(
self
,
view
):
return
self
.
options
def
validate
(
self
,
value
):
return
True
def
apply
(
self
,
query
):
raise
NotImplemented
()
def
__unicode__
(
self
):
return
self
.
name
# Customized filters
class
BaseBooleanFilter
(
BaseFilter
):
def
__init__
(
self
,
name
,
data_type
=
None
):
super
(
BaseBooleanFilter
,
self
)
.
__init__
(
name
,
((
'1'
,
'Yes'
),
(
'0'
,
'No'
)),
data_type
)
def
validate
(
self
,
value
):
return
value
==
'0'
or
value
==
'1'
class
BaseDateFilter
(
BaseFilter
):
def
__init__
(
self
,
name
,
options
=
None
):
super
(
BaseDateFilter
,
self
)
.
__init__
(
name
,
options
,
data_type
=
'datepicker'
)
def
validate
(
self
,
value
):
# TODO: Validation
return
True
class
BaseDateTimeFilter
(
BaseFilter
):
def
__init__
(
self
,
name
,
options
=
None
):
super
(
BaseDateTimeFilter
,
self
)
.
__init__
(
name
,
options
,
data_type
=
'datetimepicker'
)
def
validate
(
self
,
value
):
# TODO: Validation
return
True
def
convert
(
*
args
):
def
_inner
(
func
):
print
args
func
.
_converter_for
=
args
return
func
return
_inner
class
BaseFilterConverter
(
object
):
def
__init__
(
self
):
self
.
converters
=
dict
()
for
p
in
dir
(
self
):
attr
=
getattr
(
self
,
p
)
if
hasattr
(
attr
,
'_converter_for'
):
for
p
in
attr
.
_converter_for
:
self
.
converters
[
p
]
=
attr
flask_adminex/static/css/admin.css
View file @
73ed6524
/*
Body
*/
/*
Global styles
*/
body
body
{
{
padding-top
:
50px
;
padding-top
:
50px
;
}
}
/* Form customizations */
form
.icon
{
form
.icon
{
display
:
inline
;
display
:
inline
;
}
}
...
@@ -19,3 +20,18 @@ form.icon button {
...
@@ -19,3 +20,18 @@ form.icon button {
a
.icon
{
a
.icon
{
text-decoration
:
none
;
text-decoration
:
none
;
}
}
/* Filters */
.filter-row
{
margin
:
4px
;
}
.filter-row
a
,
.filter-row
select
{
margin-right
:
4px
;
}
.filter-row
input
{
margin-bottom
:
0px
;
width
:
208px
;
}
\ No newline at end of file
flask_adminex/static/js/filters.js
0 → 100644
View file @
73ed6524
var
Filters
=
function
(
element
,
operations
,
options
,
types
)
{
var
$root
=
$
(
element
)
var
$container
=
$
(
'#filters'
);
var
count
=
$
(
'#filters>div'
,
$root
).
length
;
function
appendValueControl
(
element
,
id
,
optionId
)
{
var
field
;
// Conditionally generate select or textbox
if
(
optionId
in
options
)
{
field
=
$
(
'<select class="filter-val" />'
).
attr
(
'name'
,
'flt'
+
id
+
'v'
);
$
(
options
[
optionId
]).
each
(
function
()
{
field
.
append
(
$
(
'<option/>'
).
val
(
this
[
0
]).
text
(
this
[
1
]));
});
}
else
{
field
=
$
(
'<input type="text" class="filter-val" />'
).
attr
(
'name'
,
'flt'
+
id
+
'v'
);
}
$
(
element
).
append
(
field
);
if
(
optionId
in
options
)
field
.
chosen
();
if
(
optionId
in
types
)
{
field
.
attr
(
'data-role'
,
types
[
optionId
]);
adminForm
.
applyStyle
(
field
,
types
[
optionId
]);
}
}
function
addFilter
()
{
var
node
=
$
(
'<div class="filter-row" />'
).
attr
(
'id'
,
'fltdiv'
+
count
).
appendTo
(
$container
);
$
(
'<a href="#" class="remove-filter" />'
)
.
append
(
'<i class="icon-remove"/>'
)
.
click
(
removeFilter
)
.
appendTo
(
node
);
var
operation
=
$
(
'<select class="filter-op" />'
)
.
attr
(
'name'
,
'flt'
+
count
)
.
change
(
changeOperation
)
.
appendTo
(
node
);
var
index
=
0
;
$
(
operations
).
each
(
function
()
{
operation
.
append
(
$
(
'<option/>'
).
val
(
index
).
text
(
this
.
toString
()));
index
++
;
});
operation
.
chosen
();
appendValueControl
(
node
,
count
,
0
);
count
+=
1
;
$
(
'button'
,
$root
).
show
();
return
false
;
}
function
removeFilter
()
{
var
row
=
$
(
this
).
parent
();
var
idx
=
parseInt
(
row
.
attr
(
'id'
).
substr
(
6
));
// Remove row
row
.
remove
();
// Renumber any rows that are after
for
(
var
i
=
idx
+
1
;
i
<
count
;
++
i
)
{
row
=
$
(
'#fltdiv'
+
i
);
row
.
attr
(
'id'
,
'fltdiv'
+
(
i
-
1
));
$
(
'.filter-op'
,
row
).
attr
(
'name'
,
'flt'
+
(
i
-
1
));
$
(
'.filter-val'
,
row
).
attr
(
'name'
,
'flt'
+
(
i
-
1
)
+
'v'
);
}
count
-=
1
;
$
(
'button'
,
$root
).
show
();
return
false
;
}
function
changeOperation
()
{
var
row
=
$
(
this
).
parent
();
var
rowIdx
=
parseInt
(
row
.
attr
(
'id'
).
substr
(
6
));
// Get old value field
var
oldValue
=
$
(
'.filter-val'
,
row
);
var
oldValueId
=
oldValue
.
attr
(
'id'
);
// Delete old value
oldValue
.
remove
();
if
(
oldValueId
!=
null
)
$
(
'div#'
+
oldValueId
+
'_chzn'
,
row
).
remove
();
var
optId
=
$
(
this
).
val
();
appendValueControl
(
row
,
rowIdx
,
optId
);
$
(
'button'
,
$root
).
show
();
};
$
(
'#add_filter'
,
$root
).
click
(
addFilter
);
$
(
'.remove-filter'
,
$root
).
click
(
removeFilter
);
$
(
'.filter-op'
).
change
(
changeOperation
);
$
(
'.filter-val'
).
change
(
function
()
{
$
(
'button'
,
$root
).
show
();
});
};
flask_adminex/static/js/form.js
View file @
73ed6524
$
(
function
()
{
var
adminForm
=
new
function
()
{
this
.
applyStyle
=
function
(
el
,
name
)
{
switch
(
name
)
{
case
'chosen'
:
$
(
el
).
chosen
();
break
;
case
'chosenblank'
:
$
(
el
).
chosen
({
allow_single_deselect
:
true
});
break
;
case
'datepicker'
:
$
(
el
).
datepicker
();
break
;
case
'datetimepicker'
:
$
(
el
).
datepicker
({
displayTime
:
true
});
break
;
};
}
// Apply automatic styles
$
(
'[data-role=chosen]'
).
chosen
();
$
(
'[data-role=chosen]'
).
chosen
();
$
(
'[data-role=chosenblank]'
).
chosen
({
allow_single_deselect
:
true
});
$
(
'[data-role=chosenblank]'
).
chosen
({
allow_single_deselect
:
true
});
$
(
'[data-role=datepicker]'
).
datepicker
();
$
(
'[data-role=datepicker]'
).
datepicker
();
$
(
'[data-role=datetimepicker]'
).
datepicker
({
displayTime
:
true
});
$
(
'[data-role=datetimepicker]'
).
datepicker
({
displayTime
:
true
});
}
);
}
flask_adminex/templates/admin/model/list.html
View file @
73ed6524
{% extends 'admin/master.html' %}
{% extends 'admin/master.html' %}
{% import 'admin/lib.html' as lib %}
{% import 'admin/lib.html' as lib %}
{% block head %}
<link
href=
"{{ url_for('admin.static', filename='chosen/chosen.css') }}"
rel=
"stylesheet"
>
<link
href=
"{{ url_for('admin.static', filename='css/datepicker.css') }}"
rel=
"stylesheet"
>
{% endblock %}
{% block body %}
{% block body %}
{% if search_supported %}
{% if search_supported %}
<form
method=
"GET"
action=
"{{ return_url }}"
class=
"well form-search"
>
<form
method=
"GET"
action=
"{{ return_url }}"
class=
"well form-search"
>
...
@@ -20,6 +25,38 @@
...
@@ -20,6 +25,38 @@
</form>
</form>
{% endif %}
{% endif %}
{% if filter_names %}
<form
id=
"filter_form"
method=
"GET"
action=
"{{ return_url }}"
class=
"well"
>
<div
id=
"filters"
>
{%- for idx, flt in enumerate(active_filters) -%}
<div
id=
"fltdiv{{ idx }}"
class=
"filter-row"
>
<a
href=
"#"
class=
"remove-filter"
><i
class=
"icon-remove"
></i></a><select
name=
"flt{{ idx }}"
class=
"filter-op"
data-role=
"chosen"
>
{% for optidx, opt in enumerate(filter_names) -%}
<option
value=
"{{ optidx }}"
{%
if
flt
[
0
]
==
optidx
%}
selected=
"selected"
{%
endif
%}
>
{{ opt }}
</option>
{%- endfor %}
</select>
{%- set data = filter_data.get(flt[0]) -%}
{%- if data -%}
<select
name=
"flt{{ idx }}v"
class=
"filter-val"
data-role=
"chosen"
>
{%- for opt in data %}
<option
value=
"{{ opt[0] }}"
{%
if
flt
[
1
]
==
opt
[
0
]
%}
selected
{%
endif
%}
>
{{ opt[1] }}
</option>
{%- endfor %}
</select>
{%- else -%}
<input
name=
"flt{{ idx }}v"
type=
"text"
value=
"{{ flt[1] or '' }}"
class=
"filter-val"
{%
if
flt
[
0
]
in
filter_types
%}
data-role=
"{{ filter_types[flt[0]] }}"
{%
endif
%}
></input>
{%- endif -%}
</div>
{%- endfor %}
</div>
{% if active_filters %}
<a
href=
"{{ clear_search_url }}"
class=
"btn"
>
Reset Filters
</a>
{% endif %}
<a
id=
"add_filter"
href=
"#"
class=
"btn"
>
Add Filter
</a>
<button
type=
"submit"
class=
"btn"
style=
"display: none"
>
Apply
</button>
</form>
{% endif %}
<table
class=
"table table-striped table-bordered model-list"
>
<table
class=
"table table-striped table-bordered model-list"
>
<thead>
<thead>
<tr>
<tr>
...
@@ -75,3 +112,17 @@
...
@@ -75,3 +112,17 @@
<a
class=
"btn btn-primary btn-large"
href=
"{{ url_for('.create_view', url=return_url) }}"
>
Create New
</a>
<a
class=
"btn btn-primary btn-large"
href=
"{{ url_for('.create_view', url=return_url) }}"
>
Create New
</a>
{% endif %}
{% endif %}
{% endblock %}
{% endblock %}
{% block tail %}
<script
src=
"{{ url_for('admin.static', filename='js/bootstrap-datepicker.js') }}"
></script>
<script
src=
"{{ url_for('admin.static', filename='js/form.js') }}"
></script>
<script
src=
"{{ url_for('admin.static', filename='js/filters.js') }}"
></script>
{% if filter_names is not none and filter_data is not none %}
<script
language=
"javascript"
>
var
filter
=
new
Filters
(
'#filter_form'
,
{{
filter_names
|
tojson
|
safe
}},
{{
filter_data
|
tojson
|
safe
}},
{{
filter_types
|
tojson
|
safe
}});
</script>
{% endif %}
{% endblock %}
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