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 @@
- Calendar - add validation for time without seconds (automatically add seconds)
- Model Admin
- Ability to sort by fields that are not visible?
- Exclude for list columns
- Exclude for form fields
- List display callables
- Search
- Rename init_search
- Built-in filtering support
- Configurable operations (=, >, <, etc)
- Callable operations
...
...
@@ -20,5 +20,6 @@
- Header title
- Mass-delete functionality
- File size restriction
- Localization
- Unit tests
- Documentation
examples/sqla/simple.py
View file @
73ed6524
...
...
@@ -3,6 +3,7 @@ from flaskext.sqlalchemy import SQLAlchemy
from
flask.ext
import
adminex
,
wtf
from
flask.ext.adminex.ext
import
sqlamodel
from
flask.ext.adminex.ext.sqlamodel
import
filters
# Create application
app
=
Flask
(
__name__
)
...
...
@@ -61,6 +62,8 @@ class PostAdmin(sqlamodel.ModelView):
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
# be 'Big Text' and add required() validator.
form_args
=
dict
(
...
...
@@ -84,4 +87,4 @@ if __name__ == '__main__':
# Start app
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
from
flask.ext.adminex
import
form
from
flask.ext.adminex.model
import
BaseModelView
from
flask.ext.adminex.ext.sqlamodel
import
filters
,
tools
class
Unique
(
object
):
...
...
@@ -220,6 +221,11 @@ class ModelView(BaseModelView):
For example, if you entered *=ZZZ*, *ILIKE 'ZZZ'* statement will be used.
"""
filter_converter
=
filters
.
FilterConverter
()
"""
TBD:
"""
def
__init__
(
self
,
model
,
session
,
name
=
None
,
category
=
None
,
endpoint
=
None
,
url
=
None
):
"""
...
...
@@ -241,8 +247,9 @@ class ModelView(BaseModelView):
self
.
session
=
session
self
.
_search_fields
=
None
self
.
_search_joins
=
None
self
.
_search_joins_names
=
None
self
.
_search_joins_names
=
set
()
self
.
_filter_joins_names
=
set
()
super
(
ModelView
,
self
)
.
__init__
(
model
,
name
,
category
,
endpoint
,
url
)
...
...
@@ -312,6 +319,23 @@ class ModelView(BaseModelView):
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
):
"""
Initialize search. Returns `True` if search is supported for this
...
...
@@ -322,23 +346,10 @@ class ModelView(BaseModelView):
"""
if
self
.
searchable_columns
:
self
.
_search_fields
=
[]
self
.
_search_joins
=
[]
self
.
_search_joins_names
=
set
()
for
p
in
self
.
searchable_columns
:
# If item is a stirng, resolve it as an attribute
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
:
for
column
in
self
.
_get_columns_for_field
(
p
):
column_type
=
type
(
column
.
type
)
.
__name__
if
not
self
.
is_text_column_type
(
column_type
):
...
...
@@ -349,7 +360,6 @@ class ModelView(BaseModelView):
# If it belongs to different table - add a join
if
column
.
table
!=
self
.
model
.
__table__
:
self
.
_search_joins
.
append
(
column
.
table
)
self
.
_search_joins_names
.
add
(
column
.
table
.
name
)
return
bool
(
self
.
searchable_columns
)
...
...
@@ -363,6 +373,31 @@ class ModelView(BaseModelView):
return
(
name
==
'String'
or
name
==
'Unicode'
or
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
):
"""
Create form from the model.
...
...
@@ -395,7 +430,7 @@ class ModelView(BaseModelView):
return
joined
# 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.
...
...
@@ -409,6 +444,8 @@ class ModelView(BaseModelView):
Search query
`execute`
Execute query immediately? Default is `True`
`filters`
List of filter tuples
"""
# Will contain names of joined tables to avoid duplicate joins
...
...
@@ -416,11 +453,11 @@ class ModelView(BaseModelView):
query
=
self
.
session
.
query
(
self
.
model
)
# Apply search
before counting results
# Apply search
criteria
if
self
.
_search_supported
and
search
:
# Apply search-related joins
if
self
.
_search_joins
:
query
=
query
.
join
(
*
self
.
_search_joins
)
if
self
.
_search_joins
_names
:
query
=
query
.
join
(
*
self
.
_search_joins
_names
)
joins
|=
self
.
_search_joins_names
# Apply terms
...
...
@@ -430,16 +467,24 @@ class ModelView(BaseModelView):
if
not
term
:
continue
if
term
.
startswith
(
'^'
):
stmt
=
'
%
s
%%
'
%
term
[
1
:]
elif
term
.
startswith
(
'='
):
stmt
=
term
[
1
:]
else
:
stmt
=
'
%%%
s
%%
'
%
term
stmt
=
tools
.
parse_like_term
(
term
)
filter_stmt
=
[
c
.
ilike
(
stmt
)
for
c
in
self
.
_search_fields
]
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
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
.base
import
BaseView
,
expose
from
flask.ext.adminex.base
import
BaseView
,
expose
from
flask.ext.adminex.model
import
filters
class
BaseModelView
(
BaseView
):
...
...
@@ -98,8 +101,8 @@ class BaseModelView(BaseView):
searchable_columns
=
None
"""
Collection of the searchable columns. It is assumed that only
text-only fields are searchable, but it is up for a model
implementation
to make decision.
text-only fields are searchable, but it is up for a model
implementation
to make decision.
For example::
...
...
@@ -107,6 +110,13 @@ class BaseModelView(BaseView):
searchable_columns = ('name', 'email')
"""
column_filters
=
None
"""
Collection of the column filters.
TBD: Doc
"""
form_columns
=
None
"""
Collection of the model field names for the form. If set to `None` will
...
...
@@ -186,14 +196,29 @@ class BaseModelView(BaseView):
"""
Refresh various cached variables.
"""
# List view
self
.
_list_columns
=
self
.
get_list_columns
()
self
.
_sortable_columns
=
self
.
get_sortable_columns
()
# Forms
self
.
_create_form_class
=
self
.
get_create_form
()
self
.
_edit_form_class
=
self
.
get_edit_form
()
# 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
def
scaffold_list_columns
(
self
):
"""
...
...
@@ -207,26 +232,30 @@ class BaseModelView(BaseView):
"""
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
):
"""
Returns list of the model field names. If `list_columns` was
set, returns it. Otherwise calls `scaffold_list_columns`
to generate list from the model.
"""
result
=
[]
if
self
.
list_columns
is
None
:
columns
=
self
.
scaffold_list_columns
()
else
:
columns
=
self
.
list_columns
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
return
[(
c
,
self
.
get_column_name
(
c
))
for
c
in
columns
]
def
scaffold_sortable_columns
(
self
):
"""
...
...
@@ -266,10 +295,51 @@ class BaseModelView(BaseView):
"""
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
):
"""
Create `form.BaseForm` inherited class from the model. Must be
implemented in
the child class.
Create `form.BaseForm` inherited class from the model. Must be
implemented in
the child class.
"""
raise
NotImplemented
(
'Please implement scaffold_form method'
)
...
...
@@ -325,7 +395,7 @@ class BaseModelView(BaseView):
return
self
.
_list_columns
[
idx
]
# 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
and sorting.
...
...
@@ -340,6 +410,9 @@ class BaseModelView(BaseView):
If set to True, sorting is in descending order.
`search`
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'
)
...
...
@@ -418,9 +491,28 @@ class BaseModelView(BaseView):
sort_desc
=
request
.
args
.
get
(
'desc'
,
None
,
type
=
int
)
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
other parameters.
...
...
@@ -435,6 +527,8 @@ class BaseModelView(BaseView):
Use descending sorting order
`search`
Search query
`filters`
List of active filters
"""
if
not
search
:
search
=
None
...
...
@@ -442,11 +536,16 @@ class BaseModelView(BaseView):
if
not
page
:
page
=
None
return
url_for
(
view
,
page
=
page
,
sort
=
sort
,
desc
=
sort_desc
,
search
=
search
)
kwargs
=
dict
(
page
=
page
,
sort
=
sort
,
desc
=
sort_desc
,
search
=
search
)
if
filters
:
for
i
,
flt
in
enumerate
(
filters
):
base
=
'flt
%
d'
%
i
kwargs
[
base
]
=
flt
[
0
]
kwargs
[
base
+
'v'
]
=
flt
[
1
]
return
url_for
(
view
,
**
kwargs
)
# Views
@
expose
(
'/'
)
...
...
@@ -455,7 +554,7 @@ class BaseModelView(BaseView):
List view
"""
# 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
sort_column
=
self
.
_get_column_by_idx
(
sort_idx
)
...
...
@@ -463,20 +562,34 @@ class BaseModelView(BaseView):
sort_column
=
sort_column
[
0
]
# 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
num_pages
=
count
/
self
.
page_size
if
count
%
self
.
page_size
!=
0
:
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
def
pager_url
(
p
):
# Do not add page number if it is first page
if
p
==
0
:
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
):
desc
=
None
...
...
@@ -484,7 +597,8 @@ class BaseModelView(BaseView):
if
invert
and
not
sort_desc
:
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
):
return
getattr
(
obj
,
field
,
None
)
...
...
@@ -495,12 +609,14 @@ class BaseModelView(BaseView):
list_columns
=
self
.
_list_columns
,
sortable_columns
=
self
.
_sortable_columns
,
# Stuff
enumerate
=
enumerate
,
get_value
=
get_value
,
return_url
=
self
.
_get_url
(
'.index_view'
,
page
,
sort_idx
,
sort_desc
,
search
),
search
,
filters
),
# Pagination
pager_url
=
pager_url
,
num_pages
=
num_pages
,
...
...
@@ -514,9 +630,13 @@ class BaseModelView(BaseView):
clear_search_url
=
self
.
_get_url
(
'.index_view'
,
None
,
sort_idx
,
sort_desc
,
None
),
search
=
search
sort_desc
),
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'
))
...
...
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
{
padding-top
:
50px
;
}
/* Form customizations */
form
.icon
{
display
:
inline
;
}
...
...
@@ -19,3 +20,18 @@ form.icon button {
a
.icon
{
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=chosenblank]'
).
chosen
({
allow_single_deselect
:
true
});
$
(
'[data-role=datepicker]'
).
datepicker
();
$
(
'[data-role=datetimepicker]'
).
datepicker
({
displayTime
:
true
});
}
);
}
flask_adminex/templates/admin/model/list.html
View file @
73ed6524
{% extends 'admin/master.html' %}
{% 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 %}
{% if search_supported %}
<form
method=
"GET"
action=
"{{ return_url }}"
class=
"well form-search"
>
...
...
@@ -20,6 +25,38 @@
</form>
{% 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"
>
<thead>
<tr>
...
...
@@ -75,3 +112,17 @@
<a
class=
"btn btn-primary btn-large"
href=
"{{ url_for('.create_view', url=return_url) }}"
>
Create New
</a>
{% endif %}
{% 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