Commit b2ea5731 authored by bryhoyt's avatar bryhoyt

Merge pull request #9 from mrjoes/master

Merge from central repo into bryhoyt's fork
parents 6c53c78b c8132741
......@@ -36,8 +36,12 @@ Several usage examples are included in the */examples* folder. Please feel free
on some of the existing ones, and then submit them via GitHub as a *pull-request*.
You can see some of these examples in action at `http://examples.flask-admin.org <http://examples.flask-admin.org/>`_.
To run that same page in your local environment, simply::
To run one of the examples in your local environment, simply::
cd flask-admin
python examples/runserver.py
Alternatively, you can run the examples one at a time, with something like::
cd flask-admin
python examples/simple/simple.py
......
__author__ = 'petrus'
from flask import Flask
from flask import render_template
app = Flask(__name__)
app.debug = True
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # 5Mb
@app.route('/')
def index():
return render_template('index.html')
if __name__ == '__main__':
app.run()
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
<!DOCTYPE html>
<html>
<head>
<title>Flask-Admin examples</title>
<meta name="description" content="Live examples of the Flask-Admin package in action. See how you can add your own custom views, change the look & feel of the admin interface, or just add basic CRUD-views for managing your models.">
<link rel="stylesheet" href="static/bootstrap/css/bootstrap.css">
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<style>
.item{
padding-top: 5px;
}
.item hr{
margin-bottom: 0;
}
.item .btn{
margin-bottom: 10px;
}
.item p.lead{
margin-bottom: 5px;
}
.footer p{
margin-top: 25px;
margin-bottom: 45px;
}
</style>
</head>
<body>
<div class="jumbotron">
<h1>Flask-Admin examples</h1>
<p>
These are some of the examples that can be found in the Flask-Admin GitHub repo at
<a href="https://github.com/mrjoes/flask-admin" target="_blank">https://github.com/mrjoes/flask-admin</a>.
Feel free to play around. This site gets refreshed every 10 minutes or so.
</p>
</div>
<div class="container">
<div class="item">
<h2>Simple views</h2>
<p class="lead">Add a few of your own views to the admin interface. You can add links to them in the top navbar,
but you don't have to.</p>
<a class="btn btn-primary" role="button" href="simple/admin/"><i class="fa fa-chevron-right"></i> view example</a>
<hr>
</div>
<div class="item">
<h2>SQLAlchemy models</h2>
<p class="lead">Add some basic CRUD-views for your models.</p>
<a class="btn btn-primary" role="button" href="sqla/simple/admin/"><i class="fa fa-chevron-right"></i> view example</a>
<!--<p class="lead">Define models with multiple primary keys.</p>-->
<!--<a class="btn btn-primary" role="button" href="sqla/multiple_pk/admin/"><i class="fa fa-chevron-right"></i> view example</a>-->
<hr>
</div>
<div class="item">
<h2>Customize the layout</h2>
<p class="lead">Take control of the look & feel of your admin interface.</p>
<a class="btn btn-primary" role="button" href="layout/admin/"><i class="fa fa-chevron-right"></i> view example</a>
<hr>
</div>
<div class="item">
<h2>Files, images & custom forms</h2>
<p class="lead">Define custom forms using form rules, and quickly add file/image management to your application.</p>
<p>Note: a 5Mb limit has been placed on the size of uploaded files & images for this example.</p>
<a class="btn btn-primary" role="button" href="forms/admin/"><i class="fa fa-chevron-right"></i> view example</a>
<hr>
</div>
<div class="item">
<h2>Authentication</h2>
<p class="lead">Use Flask-Login to authenticate users.</p>
<a class="btn btn-primary" role="button" href="auth/admin/"><i class="fa fa-chevron-right"></i> view example</a>
<hr>
</div>
</div>
<div class="container footer">
<p>
</p>
</div>
<!-- Google Analytics tracking -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-45533714-1', 'flask-admin.org');
ga('send', 'pageview');
</script>
</body>
</html>
\ No newline at end of file
from werkzeug.wsgi import DispatcherMiddleware
from werkzeug.serving import run_simple
from index.index import app as index
import examples.simple.simple
import examples.sqla.simple
import examples.layout.simple
import examples.forms.simple
import examples.auth.auth
examples.sqla.simple.build_sample_db()
examples.layout.simple.build_sample_db()
examples.forms.simple.build_sample_db()
examples.auth.auth.build_sample_db()
application = DispatcherMiddleware(
index,
{
'/simple': examples.simple.simple.app,
'/sqla/simple': examples.sqla.simple.app,
'/layout': examples.layout.simple.app,
'/forms': examples.forms.simple.app,
'/auth': examples.auth.auth.app,
}
)
if __name__ == '__main__':
run_simple('localhost', 5000, application,
use_reloader=True, use_debugger=True, use_evalex=True)
\ No newline at end of file
......@@ -72,3 +72,22 @@ def with_metaclass(meta, *bases):
return type.__new__(cls, name, (), d)
return meta(name, bases, d)
return metaclass('temporary_class', None, {})
try:
from collections import OrderedDict
except ImportError:
# Bare-bones OrderedDict implementation for Python2.6 compatibility
class OrderedDict(dict):
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
self.ordered_keys = []
def __setitem__(self, key, value):
self.ordered_keys.append(key)
dict.__setitem__(self, key, value)
def __iter__(self):
return (k for k in self.ordered_keys)
def iteritems(self):
return ((k, self[k]) for k in self.ordered_keys)
def items(self):
return list(self.iteritems())
......@@ -4,6 +4,7 @@ from wtforms import fields
from wtforms.fields.core import _unset_value
from . import widgets
from flask.ext.admin.model.fields import InlineFormField
def is_empty(file_object):
......@@ -13,15 +14,16 @@ def is_empty(file_object):
return not bool(first_char)
class ModelFormField(fields.FormField):
class ModelFormField(InlineFormField):
"""
Customized ModelFormField for MongoEngine EmbeddedDocuments.
"""
def __init__(self, model, view, *args, **kwargs):
super(ModelFormField, self).__init__(*args, **kwargs)
def __init__(self, model, view, form_class, form_opts=None, **kwargs):
super(ModelFormField, self).__init__(form_class, **kwargs)
self.model = model
self.view = view
self.form_opts = form_opts
def populate_obj(self, obj, name):
candidate = getattr(obj, name, None)
......
......@@ -137,11 +137,8 @@ class CustomModelConverter(orm.ModelConverter):
view = self._get_subdocument_config(field.name)
if 'widget' not in kwargs:
form_opts = form.FormOpts(widget_args=getattr(view, 'form_widget_args', None),
form_rules=view._form_rules)
kwargs['widget'] = InlineFormWidget(form_opts)
form_opts = form.FormOpts(widget_args=getattr(view, 'form_widget_args', None),
form_rules=view._form_rules)
form_class = view.get_form()
if form_class is None:
......@@ -155,7 +152,7 @@ class CustomModelConverter(orm.ModelConverter):
form_class = view.postprocess_form(form_class)
return ModelFormField(field.document_type_obj, view, form_class, **kwargs)
return ModelFormField(field.document_type_obj, view, form_class, form_opts=form_opts, **kwargs)
@orm.converts('ReferenceField')
def conv_Reference(self, model, field, kwargs):
......
......@@ -171,7 +171,7 @@ class InlineModelConverter(InlineModelConverterBase):
attrs = dict()
for attr in dir(p):
if not attr.startswith('_') and attr != model:
if not attr.startswith('_') and attr != 'model':
attrs[attr] = getattr(p, attr)
info = InlineFormAdmin(model, **attrs)
......
......@@ -188,7 +188,7 @@ class InlineModelFormList(InlineFieldList):
Form field type. Override to use custom field for each inline form
"""
def __init__(self, form, session, model, prop, inline_view, form_widget=None, **kwargs):
def __init__(self, form, session, model, prop, inline_view, **kwargs):
"""
Default constructor.
......@@ -212,13 +212,10 @@ class InlineModelFormList(InlineFieldList):
self._pk = get_primary_key(model)
# Generate inline form field
if form_widget is None:
form_opts = FormOpts(widget_args=getattr(inline_view, 'form_widget_args', None),
form_rules=inline_view._form_rules)
form_opts = FormOpts(widget_args=getattr(inline_view, 'form_widget_args', None),
form_rules=inline_view._form_rules)
form_widget = InlineFormWidget(form_opts)
form_field = self.form_field_type(form, self._pk, widget=form_widget)
form_field = self.form_field_type(form, self._pk, form_opts=form_opts)
super(InlineModelFormList, self).__init__(form_field, **kwargs)
......
......@@ -95,6 +95,11 @@ class AdminModelConverter(ModelConverterBase):
return QuerySelectField(**kwargs)
def _convert_relation(self, prop, kwargs):
# Check if relation is specified
form_columns = getattr(self.view, 'form_columns', None)
if form_columns and prop.key not in form_columns:
return None
remote_model = prop.mapper.class_
column = prop.local_remote_pairs[0][0]
......
......@@ -32,6 +32,12 @@ def get_primary_key(model):
pks.append(get_column_for_current_model(p).key)
else:
pks.append(p.key)
else:
if hasattr(p, 'columns'):
for c in p.columns:
if c.primary_key:
pks.append(p.key)
break
if len(pks) == 1:
return pks[0]
......@@ -52,8 +58,12 @@ def is_inherited_primary_key(prop):
:return: Boolean
:raises: Exceptions as they occur - no ExceptionHandling here
"""
if not hasattr(prop, 'expression'):
return False
if prop.expression.primary_key:
return len(prop._orig_columns) == len(prop.columns)-1
return False
def get_column_for_current_model(prop):
......
......@@ -24,6 +24,10 @@ class Unique(object):
self.message = message
def __call__(self, form, field):
# databases allow multiple NULL values for unique columns
if field.data is None:
return
try:
obj = (self.db_session.query(self.model)
.filter(self.column == field.data)
......
......@@ -326,6 +326,7 @@ class ModelView(BaseModelView):
columns.append(p.key)
elif hasattr(p, 'columns'):
column_inherited_primary_key = False
if len(p.columns) != 1:
if is_inherited_primary_key(p):
column = get_column_for_current_model(p)
......
......@@ -178,7 +178,7 @@ class FileUploadField(fields.TextField):
return ('.' in filename and
filename.rsplit('.', 1)[1].lower() in
map(str.lower, self.allowed_extensions))
map(lambda x: x.lower(), self.allowed_extensions))
def pre_validate(self, form):
if (self.data and
......
import warnings
import re
from flask import request, url_for, redirect, flash, abort, json, Response
......@@ -13,11 +14,16 @@ from flask.ext.admin.actions import ActionsMixin
from flask.ext.admin.helpers import get_form_data, validate_form_on_submit, get_redirect_target
from flask.ext.admin.tools import rec_getattr
from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin._compat import iteritems, as_unicode
from flask.ext.admin._compat import iteritems, OrderedDict
from .helpers import prettify_name, get_mdict_item_or_list
from .ajax import AjaxModelLoader
# Used to generate filter query string name
filter_char_re = re.compile('[^a-z0-9 ]')
filter_compact_re = re.compile(' +')
class BaseModelView(BaseView, ActionsMixin):
"""
Base model view.
......@@ -252,6 +258,15 @@ class BaseModelView(BaseView, ActionsMixin):
column_filters = ('user', 'email')
"""
named_filter_urls = False
"""
Set to True to use human-readable names for filters in URL parameters.
False by default so as to be robust across translations.
Changing this parameter will break any existing URLs that have filters.
"""
column_display_pk = ObsoleteAttr('column_display_pk',
'list_display_pk',
False)
......@@ -543,26 +558,27 @@ class BaseModelView(BaseView, ActionsMixin):
if self.column_descriptions is None:
self.column_descriptions = dict()
# Group filters by field name
if self._filters:
self._filter_groups = []
self._filter_dict = dict()
for i, n in enumerate(self._filters):
if n.name not in self._filter_dict:
group = []
self._filter_dict[n.name] = group
self._filter_groups.append((n.name, group))
else:
group = self._filter_dict[n.name]
group.append((i, n.operation()))
self._filter_groups = OrderedDict()
self._filter_args = {}
for i, flt in enumerate(self._filters):
if flt.name not in self._filter_groups:
self._filter_groups[flt.name] = []
self._filter_groups[flt.name].append({
'index': i,
'arg': self.get_filter_arg(i, flt),
'operation': flt.operation(),
'options': flt.get_options(self) or None,
'type': flt.data_type
})
self._filter_types = dict((i, f.data_type)
for i, f in enumerate(self._filters)
if f.data_type)
self._filter_args[self.get_filter_arg(i, flt)] = (i, flt)
else:
self._filter_groups = None
self._filter_types = None
self._filter_args = None
# Form rendering rules
if self.form_create_rules:
......@@ -671,6 +687,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
return False
# Filter helpers
def scaffold_filters(self, name):
"""
Generate filter object for the given name
......@@ -715,6 +732,27 @@ class BaseModelView(BaseView, ActionsMixin):
else:
return None
def get_filter_arg(self, index, flt):
"""
Given a filter `flt`, return a unique name for that filter in
this view.
Does not include the `flt[n]_` portion of the filter name.
:param index:
Filter index in _filters array
:param flt:
Filter instance
"""
if self.named_filter_urls:
name = ('%s %s' % (flt.name, flt.operation())).lower()
name = filter_char_re.sub('', name)
name = filter_compact_re.sub('_', name)
return name
else:
return str(index)
# Form helpers
def scaffold_form(self):
"""
Create `form.BaseForm` inherited class from the model. Must be
......@@ -948,43 +986,42 @@ class BaseModelView(BaseView, ActionsMixin):
def get_empty_list_message(self):
return gettext('There are no items in the table.')
# URL generation helper
def _get_extra_args(self):
"""
Return arguments from query string.
"""
page = request.args.get('page', 0, type=int)
sort = request.args.get('sort', None, type=int)
sort_desc = request.args.get('desc', None, type=int)
search = request.args.get('search', None)
# Gather filters
# URL generation helpers
def _get_list_filter_args(self):
if self._filters:
sfilters = []
filters = []
for n in request.args:
if n.startswith('flt'):
ofs = n.find('_')
if ofs == -1:
continue
if not n.startswith('flt'):
continue
try:
pos = int(n[3:ofs])
idx = int(n[ofs + 1:])
except ValueError:
continue
if '_' not in n:
continue
if idx >= 0 and idx < len(self._filters):
flt = self._filters[idx]
pos, key = n[3:].split('_', 1)
value = request.args[n]
if key in self._filter_args:
idx, flt = self._filter_args[key]
if flt.validate(value):
sfilters.append((pos, (idx, flt.clean(value))))
value = request.args[n]
filters = [v[1] for v in sorted(sfilters, key=lambda n: n[0])]
else:
filters = None
if flt.validate(value):
filters.append((pos, (idx, flt.clean(value))))
# Sort filters
return [v[1] for v in sorted(filters, key=lambda n: n[0])]
return None
def _get_list_extra_args(self):
"""
Return arguments from query string.
"""
page = request.args.get('page', 0, type=int)
sort = request.args.get('sort', None, type=int)
sort_desc = request.args.get('desc', None, type=int)
search = request.args.get('search', None)
filters = self._get_list_filter_args()
return page, sort, sort_desc, search, filters
......@@ -1016,9 +1053,11 @@ class BaseModelView(BaseView, ActionsMixin):
kwargs = dict(page=page, sort=sort, desc=sort_desc, search=search)
if filters:
for i, flt in enumerate(filters):
key = 'flt%d_%d' % (i, flt[0])
kwargs[key] = flt[1]
for i, pair in enumerate(filters):
idx, value = pair
key = 'flt%d_%s' % (i, self.get_filter_arg(idx, self._filters[idx]))
kwargs[key] = value
return url_for(view, **kwargs)
......@@ -1038,12 +1077,6 @@ class BaseModelView(BaseView, ActionsMixin):
"""
return rec_getattr(model, name)
def _get_filter_dict(self):
"""
Return flattened filter dictionary which can be JSON-serialized.
"""
return dict((as_unicode(k), v) for k, v in iteritems(self._filter_dict))
@contextfunction
def get_list_value(self, context, model, name):
"""
......@@ -1104,7 +1137,7 @@ class BaseModelView(BaseView, ActionsMixin):
List view
"""
# Grab parameters from URL
page, sort_idx, sort_desc, search, filters = self._get_extra_args()
page, sort_idx, sort_desc, search, filters = self._get_list_extra_args()
# Map column index to column name
sort_column = self._get_column_by_idx(sort_idx)
......@@ -1120,18 +1153,6 @@ class BaseModelView(BaseView, ActionsMixin):
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
......@@ -1187,8 +1208,6 @@ class BaseModelView(BaseView, ActionsMixin):
# Filters
filters=self._filters,
filter_groups=self._filter_groups,
filter_types=self._filter_types,
filter_data=filters_data,
active_filters=filters,
# Actions
......@@ -1253,7 +1272,7 @@ class BaseModelView(BaseView, ActionsMixin):
return redirect(return_url)
form_opts = FormOpts(widget_args=self.form_widget_args,
form_rules=self._form_create_rules)
form_rules=self._form_edit_rules)
return self.render(self.edit_template,
model=model,
......
......@@ -102,10 +102,11 @@ class InlineModelFormField(FormField):
"""
widget = InlineFormWidget()
def __init__(self, form_class, pk, **kwargs):
def __init__(self, form_class, pk, form_opts=None, **kwargs):
super(InlineModelFormField, self).__init__(form_class, **kwargs)
self._pk = pk
self.form_opts = form_opts
def get_pk(self):
return getattr(self.form, self._pk).data
......
......@@ -12,12 +12,11 @@ class InlineFieldListWidget(RenderTemplateWidget):
class InlineFormWidget(RenderTemplateWidget):
def __init__(self, form_opts=None):
def __init__(self):
super(InlineFormWidget, self).__init__('admin/model/inline_form.html')
self.form_opts = form_opts
def __call__(self, field, **kwargs):
kwargs.setdefault('form_opts', self.form_opts)
kwargs.setdefault('form_opts', getattr(field, 'form_opts', None))
return super(InlineFormWidget, self).__call__(field, **kwargs)
......
......@@ -31,6 +31,10 @@ form.search-form .clear i {
margin: 2px 0 0 0;
}
form.search-form div input {
margin: 0;
}
/* Filters */
table.filters {
border-collapse: collapse;
......@@ -105,3 +109,7 @@ table.filters {
.select2-results li {
min-height: 24px !important;
}
.list-checkbox-column {
width: 14px;
}
var AdminFilters = function(element, filters_element, operations, options, types) {
var AdminFilters = function(element, filtersElement, filterGroups) {
var $root = $(element);
var $container = $('.filters', $root);
var lastCount = 0;
function getCount(name) {
var idx = name.indexOf('_');
return parseInt(name.substr(3, idx - 3));
if (idx === -1) {
return 0;
}
return parseInt(name.substr(3, idx - 3), 10);
}
function makeName(name) {
var result = 'flt' + lastCount + '_' + name;
lastCount += 1;
return result;
}
function changeOperation() {
......@@ -23,44 +34,44 @@ var AdminFilters = function(element, filters_element, operations, options, types
return false;
}
function addFilter(name, op) {
function addFilter(name, subfilters) {
var $el = $('<tr />').appendTo($container);
// Filter list
$el.append(
$('<td/>').append(
$('<a href="#" class="btn remove-filter" />')
.append($('<span class="close-icon">&times;</span>'))
.append('&nbsp;')
.append(name)
.click(removeFilter)
)
);
$('<td/>').append(
$('<a href="#" class="btn remove-filter" />')
.append($('<span class="close-icon">&times;</span>'))
.append('&nbsp;')
.append(name)
.click(removeFilter)
)
);
// Filter type
var $select = $('<select class="filter-op" />')
.change(changeOperation);
$(op).each(function() {
$select.append($('<option/>').attr('value', this[0]).text(this[1]));
$(subfilters).each(function() {
$select.append($('<option/>').attr('value', this.arg).text(this.operation));
});
$el.append(
$('<td/>').append($select)
);
$('<td/>').append($select)
);
$select.select2({width: 'resolve'});
// Input
var optId = op[0][0];
var filter = subfilters[0];
var $field;
if (optId in options) {
if (filter.options) {
$field = $('<select class="filter-val" />')
.attr('name', 'flt' + lastCount + '_' + optId);
.attr('name', makeName(filter.arg));
$(options[optId]).each(function() {
$(filter.options).each(function() {
$field.append($('<option/>')
.val(this[0]).text(this[1]));
});
......@@ -70,22 +81,20 @@ var AdminFilters = function(element, filters_element, operations, options, types
} else
{
$field = $('<input type="text" class="filter-val" />')
.attr('name', 'flt' + lastCount + '_' + optId);
.attr('name', makeName(filter.arg));
$el.append($('<td/>').append($field));
}
if (optId in types) {
$field.attr('data-role', types[optId]);
faForm.applyStyle($field, types[optId]);
if (filter.type) {
$field.attr('data-role', filter.type);
faForm.applyStyle($field, filter.type);
}
lastCount += 1;
}
$('a.filter', filters_element).click(function() {
$('a.filter', filtersElement).click(function() {
var name = $(this).text().trim();
addFilter(name, operations[name]);
addFilter(name, filterGroups[name]);
$('button', $root).show();
......
......@@ -5,7 +5,7 @@
<ul class="dropdown-menu field-filters">
{% for k in filter_groups %}
<li>
<a href="javascript:void(0)" class="filter" onclick="return false;">{{ k[0] }}</a>
<a href="javascript:void(0)" class="filter" onclick="return false;">{{ k }}</a>
</li>
{% endfor %}
</ul>
......@@ -21,31 +21,32 @@
</div>
<table class="filters">
{%- for i, flt in enumerate(active_filters) -%}
{%- for n, values in enumerate(active_filters) -%}
<tr>
{% set filter = admin_view._filters[flt[0]] %}
{% set idx, value = values %}
{% set filter = filters[idx] %}
{% set filter_arg = admin_view.get_filter_arg(idx, filter) %}
<td>
<a href="javascript:void(0)" class="btn remove-filter" title="{{ _gettext('Remove Filter') }}">
<span class="close-icon">&times;</span>&nbsp;{{ filters[flt[0]] }}
<span class="close-icon">&times;</span>&nbsp;{{ filter.name }}
</a>
</td>
<td>
<select class="filter-op" data-role="select2">
{% for op in admin_view._filter_dict[filter.name] %}
<option value="{{ op[0] }}"{% if flt[0] == op[0] %} selected="selected"{% endif %}>{{ op[1] }}</option>
{% for op in filter_groups[filter.name] %}
<option value="{{ op['arg'] }}"{% if idx == op['index'] %} selected="selected"{% endif %}>{{ op['operation'] }}</option>
{% endfor %}
</select>
</td>
<td>
{%- set data = filter_data.get(flt[0]) -%}
{%- if data -%}
<select name="flt{{ i }}_{{ flt[0] }}" class="filter-val" data-role="select2">
{%- for d in data %}
<option value="{{ d[0] }}"{% if flt[1] == d[0] %} selected{% endif %}>{{ d[1] }}</option>
{%- if filter.options -%}
<select name="flt{{n}}_{{ filter_arg }}" class="filter-val" data-role="select2">
{%- for d in filter.options %}
<option value="{{ d[0] }}"{% if value == d[0] %} selected{% endif %}>{{ d[1] }}</option>
{%- endfor %}
</select>
{%- else -%}
<input name="flt{{ i }}_{{ flt[0] }}" type="text" value="{{ flt[1] or '' }}" class="filter-val"{% if flt[0] in filter_types %} data-role="{{ filter_types[flt[0]] }}"{% endif %}></input>
<input name="flt{{n}}_{{ filter_arg }}" type="text" value="{{ value or '' }}" class="filter-val"{% if filter.data_type %} data-role="{{ filter.data_type }}"{% endif %}></input>
{%- endif -%}
</td>
</tr>
......
......@@ -22,7 +22,7 @@
</li>
{% endif %}
{% if filter_groups %}
{% if filters %}
<li class="dropdown">
{{ model_layout.filter_options() }}
</li>
......@@ -42,7 +42,7 @@
</ul>
{% endblock %}
{% if filter_groups %}
{% if filters %}
{{ model_layout.filter_form() }}
<div class="clearfix"></div>
{% endif %}
......@@ -53,7 +53,7 @@
<tr>
{% block list_header scoped %}
{% if actions %}
<th class="span1">
<th class="list-checkbox-column">
<input type="checkbox" name="rowtoggle" class="action-rowtoggle" title="{{ _gettext('Select all records') }}" />
</th>
{% endif %}
......@@ -159,12 +159,10 @@
html: true,
placement: 'bottom'
});
{% if filter_groups is not none and filter_data is not none %}
{% if filter_groups %}
var filter = new AdminFilters(
'#filter_form', '.field-filters',
{{ admin_view._get_filter_dict()|tojson|safe }},
{{ filter_data|tojson|safe }},
{{ filter_types|tojson|safe }}
{{ filter_groups|tojson|safe }}
);
{% endif %}
})(jQuery);
......
......@@ -233,61 +233,71 @@ def test_column_filters():
eq_(len(view._filters), 4)
eq_(view._filter_dict, {
u'Test1': [
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Test1']],
[
(0, u'equals'),
(1, u'not equal'),
(2, u'contains'),
(3, u'not contains')
]})
])
# Test filter that references property
view = CustomModelView(Model2, db.session,
column_filters=['model1'])
eq_(view._filter_dict, {
u'Model1 / Test1': [
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Test1']],
[
(0, u'equals'),
(1, u'not equal'),
(2, u'contains'),
(3, u'not contains')
],
'Model1 / Test2': [
])
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Test2']],
[
(4, 'equals'),
(5, 'not equal'),
(6, 'contains'),
(7, 'not contains')
],
u'Model1 / Test3': [
])
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Test3']],
[
(8, u'equals'),
(9, u'not equal'),
(10, u'contains'),
(11, u'not contains')
],
u'Model1 / Test4': [
])
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Test4']],
[
(12, u'equals'),
(13, u'not equal'),
(14, u'contains'),
(15, u'not contains')
],
u'Model1 / Bool Field': [
])
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Bool Field']],
[
(16, u'equals'),
(17, u'not equal'),
],
u'Model1 / Enum Field': [
])
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Enum Field']],
[
(18, u'equals'),
(19, u'not equal'),
]})
])
# Test filter with a dot
view = CustomModelView(Model2, db.session,
column_filters=['model1.bool_field'])
eq_(view._filter_dict, {
'Model1 / Bool Field': [
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Bool Field']],
[
(0, 'equals'),
(1, 'not equal'),
]})
])
# Fill DB
model1_obj1 = Model1('model1_obj1', bool_field=True)
......@@ -324,11 +334,15 @@ def test_column_filters():
column_filters=['int_field'])
admin.add_view(view)
eq_(view._filter_dict, {'Int Field': [(0, 'equals'), (1, 'not equal'),
(2, 'greater than'), (3, 'smaller than')]})
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']],
[
(0, 'equals'),
(1, 'not equal'),
(2, 'greater than'),
(3, 'smaller than')
])
#Test filters to joined table field
# Test filters to joined table field
view = CustomModelView(
Model2, db.session,
endpoint='_model2',
......@@ -349,6 +363,21 @@ def test_column_filters():
ok_('model2_obj3' not in data)
ok_('model2_obj4' not in data)
# Test human readable URLs
view = CustomModelView(
Model1, db.session,
column_filters=['test1'],
endpoint='_model3',
named_filter_urls=True
)
admin.add_view(view)
rv = client.get('/admin/_model3/?flt1_test1_equals=model1_obj1')
eq_(rv.status_code, 200)
data = rv.data.decode('utf-8')
ok_('model1_obj1' in data)
ok_('model1_obj2' not in data)
def test_url_args():
app, db, admin = setup()
......
......@@ -302,8 +302,8 @@ def test_column_filters():
eq_(view._filters[0].name, 'col1')
eq_(view._filters[1].name, 'col2')
eq_(view._filter_dict, {'col1': [(0, 'test')],
'col2': [(1, 'test')]})
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'col1']], [(0, 'test')])
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'col2']], [(1, 'test')])
# TODO: Make calls with filters
......
......@@ -189,6 +189,10 @@ msgstr "Уже существует."
msgid "Model was successfully created."
msgstr "Запись была создана."
#: ../flask_admin/model/base.py:1268
msgid "Model was successfully saved."
msgstr "Запись была сохранена."
#: ../flask_admin/model/filters.py:82
msgid "Yes"
msgstr "Да"
......
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