Commit 73ed6524 authored by Serge S. Koval's avatar Serge S. Koval

Started working on the filters.

parent 0edfb4a9
...@@ -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
...@@ -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)
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]
def parse_like_term(term):
if term.startswith('^'):
stmt = '%s%%' % term[1:]
elif term.startswith('='):
stmt = term[1:]
else:
stmt = '%%%s%%' % term
return stmt
...@@ -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()
......
from .base import BaseModelView
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'))
......
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
/* 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
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();
});
};
$(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});
}); }
{% 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 %}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment