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 ...@@ -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*. 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/>`_. 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 cd flask-admin
python examples/simple/simple.py 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): ...@@ -72,3 +72,22 @@ def with_metaclass(meta, *bases):
return type.__new__(cls, name, (), d) return type.__new__(cls, name, (), d)
return meta(name, bases, d) return meta(name, bases, d)
return metaclass('temporary_class', None, {}) 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 ...@@ -4,6 +4,7 @@ from wtforms import fields
from wtforms.fields.core import _unset_value from wtforms.fields.core import _unset_value
from . import widgets from . import widgets
from flask.ext.admin.model.fields import InlineFormField
def is_empty(file_object): def is_empty(file_object):
...@@ -13,15 +14,16 @@ def is_empty(file_object): ...@@ -13,15 +14,16 @@ def is_empty(file_object):
return not bool(first_char) return not bool(first_char)
class ModelFormField(fields.FormField): class ModelFormField(InlineFormField):
""" """
Customized ModelFormField for MongoEngine EmbeddedDocuments. Customized ModelFormField for MongoEngine EmbeddedDocuments.
""" """
def __init__(self, model, view, *args, **kwargs): def __init__(self, model, view, form_class, form_opts=None, **kwargs):
super(ModelFormField, self).__init__(*args, **kwargs) super(ModelFormField, self).__init__(form_class, **kwargs)
self.model = model self.model = model
self.view = view self.view = view
self.form_opts = form_opts
def populate_obj(self, obj, name): def populate_obj(self, obj, name):
candidate = getattr(obj, name, None) candidate = getattr(obj, name, None)
......
...@@ -137,11 +137,8 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -137,11 +137,8 @@ class CustomModelConverter(orm.ModelConverter):
view = self._get_subdocument_config(field.name) 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_opts = form.FormOpts(widget_args=getattr(view, 'form_widget_args', None), form_rules=view._form_rules)
form_rules=view._form_rules)
kwargs['widget'] = InlineFormWidget(form_opts)
form_class = view.get_form() form_class = view.get_form()
if form_class is None: if form_class is None:
...@@ -155,7 +152,7 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -155,7 +152,7 @@ class CustomModelConverter(orm.ModelConverter):
form_class = view.postprocess_form(form_class) 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') @orm.converts('ReferenceField')
def conv_Reference(self, model, field, kwargs): def conv_Reference(self, model, field, kwargs):
......
...@@ -171,7 +171,7 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -171,7 +171,7 @@ class InlineModelConverter(InlineModelConverterBase):
attrs = dict() attrs = dict()
for attr in dir(p): for attr in dir(p):
if not attr.startswith('_') and attr != model: if not attr.startswith('_') and attr != 'model':
attrs[attr] = getattr(p, attr) attrs[attr] = getattr(p, attr)
info = InlineFormAdmin(model, **attrs) info = InlineFormAdmin(model, **attrs)
......
...@@ -188,7 +188,7 @@ class InlineModelFormList(InlineFieldList): ...@@ -188,7 +188,7 @@ class InlineModelFormList(InlineFieldList):
Form field type. Override to use custom field for each inline form 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. Default constructor.
...@@ -212,13 +212,10 @@ class InlineModelFormList(InlineFieldList): ...@@ -212,13 +212,10 @@ class InlineModelFormList(InlineFieldList):
self._pk = get_primary_key(model) self._pk = get_primary_key(model)
# Generate inline form field # Generate inline form field
if form_widget is None: form_opts = FormOpts(widget_args=getattr(inline_view, 'form_widget_args', None),
form_opts = FormOpts(widget_args=getattr(inline_view, 'form_widget_args', None), form_rules=inline_view._form_rules)
form_rules=inline_view._form_rules)
form_widget = InlineFormWidget(form_opts) form_field = self.form_field_type(form, self._pk, form_opts=form_opts)
form_field = self.form_field_type(form, self._pk, widget=form_widget)
super(InlineModelFormList, self).__init__(form_field, **kwargs) super(InlineModelFormList, self).__init__(form_field, **kwargs)
......
...@@ -95,6 +95,11 @@ class AdminModelConverter(ModelConverterBase): ...@@ -95,6 +95,11 @@ class AdminModelConverter(ModelConverterBase):
return QuerySelectField(**kwargs) return QuerySelectField(**kwargs)
def _convert_relation(self, prop, 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_ remote_model = prop.mapper.class_
column = prop.local_remote_pairs[0][0] column = prop.local_remote_pairs[0][0]
......
...@@ -32,6 +32,12 @@ def get_primary_key(model): ...@@ -32,6 +32,12 @@ def get_primary_key(model):
pks.append(get_column_for_current_model(p).key) pks.append(get_column_for_current_model(p).key)
else: else:
pks.append(p.key) 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: if len(pks) == 1:
return pks[0] return pks[0]
...@@ -52,8 +58,12 @@ def is_inherited_primary_key(prop): ...@@ -52,8 +58,12 @@ def is_inherited_primary_key(prop):
:return: Boolean :return: Boolean
:raises: Exceptions as they occur - no ExceptionHandling here :raises: Exceptions as they occur - no ExceptionHandling here
""" """
if not hasattr(prop, 'expression'):
return False
if prop.expression.primary_key: if prop.expression.primary_key:
return len(prop._orig_columns) == len(prop.columns)-1 return len(prop._orig_columns) == len(prop.columns)-1
return False return False
def get_column_for_current_model(prop): def get_column_for_current_model(prop):
......
...@@ -24,6 +24,10 @@ class Unique(object): ...@@ -24,6 +24,10 @@ class Unique(object):
self.message = message self.message = message
def __call__(self, form, field): def __call__(self, form, field):
# databases allow multiple NULL values for unique columns
if field.data is None:
return
try: try:
obj = (self.db_session.query(self.model) obj = (self.db_session.query(self.model)
.filter(self.column == field.data) .filter(self.column == field.data)
......
...@@ -326,6 +326,7 @@ class ModelView(BaseModelView): ...@@ -326,6 +326,7 @@ class ModelView(BaseModelView):
columns.append(p.key) columns.append(p.key)
elif hasattr(p, 'columns'): elif hasattr(p, 'columns'):
column_inherited_primary_key = False column_inherited_primary_key = False
if len(p.columns) != 1: if len(p.columns) != 1:
if is_inherited_primary_key(p): if is_inherited_primary_key(p):
column = get_column_for_current_model(p) column = get_column_for_current_model(p)
......
...@@ -178,7 +178,7 @@ class FileUploadField(fields.TextField): ...@@ -178,7 +178,7 @@ class FileUploadField(fields.TextField):
return ('.' in filename and return ('.' in filename and
filename.rsplit('.', 1)[1].lower() in 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): def pre_validate(self, form):
if (self.data and if (self.data and
......
import warnings import warnings
import re
from flask import request, url_for, redirect, flash, abort, json, Response from flask import request, url_for, redirect, flash, abort, json, Response
...@@ -13,11 +14,16 @@ from flask.ext.admin.actions import ActionsMixin ...@@ -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.helpers import get_form_data, validate_form_on_submit, get_redirect_target
from flask.ext.admin.tools import rec_getattr from flask.ext.admin.tools import rec_getattr
from flask.ext.admin._backwards import ObsoleteAttr 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 .helpers import prettify_name, get_mdict_item_or_list
from .ajax import AjaxModelLoader 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): class BaseModelView(BaseView, ActionsMixin):
""" """
Base model view. Base model view.
...@@ -252,6 +258,15 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -252,6 +258,15 @@ class BaseModelView(BaseView, ActionsMixin):
column_filters = ('user', 'email') 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', column_display_pk = ObsoleteAttr('column_display_pk',
'list_display_pk', 'list_display_pk',
False) False)
...@@ -543,26 +558,27 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -543,26 +558,27 @@ class BaseModelView(BaseView, ActionsMixin):
if self.column_descriptions is None: if self.column_descriptions is None:
self.column_descriptions = dict() self.column_descriptions = dict()
# Group filters by field name
if self._filters: if self._filters:
self._filter_groups = [] self._filter_groups = OrderedDict()
self._filter_dict = dict() self._filter_args = {}
for i, n in enumerate(self._filters): for i, flt in enumerate(self._filters):
if n.name not in self._filter_dict: if flt.name not in self._filter_groups:
group = [] self._filter_groups[flt.name] = []
self._filter_dict[n.name] = group
self._filter_groups.append((n.name, group)) self._filter_groups[flt.name].append({
else: 'index': i,
group = self._filter_dict[n.name] 'arg': self.get_filter_arg(i, flt),
'operation': flt.operation(),
group.append((i, n.operation())) 'options': flt.get_options(self) or None,
'type': flt.data_type
})
self._filter_types = dict((i, f.data_type) self._filter_args[self.get_filter_arg(i, flt)] = (i, flt)
for i, f in enumerate(self._filters)
if f.data_type)
else: else:
self._filter_groups = None self._filter_groups = None
self._filter_types = None self._filter_args = None
# Form rendering rules # Form rendering rules
if self.form_create_rules: if self.form_create_rules:
...@@ -671,6 +687,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -671,6 +687,7 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
return False return False
# Filter helpers
def scaffold_filters(self, name): def scaffold_filters(self, name):
""" """
Generate filter object for the given name Generate filter object for the given name
...@@ -715,6 +732,27 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -715,6 +732,27 @@ class BaseModelView(BaseView, ActionsMixin):
else: else:
return None 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): def scaffold_form(self):
""" """
Create `form.BaseForm` inherited class from the model. Must be Create `form.BaseForm` inherited class from the model. Must be
...@@ -948,43 +986,42 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -948,43 +986,42 @@ class BaseModelView(BaseView, ActionsMixin):
def get_empty_list_message(self): def get_empty_list_message(self):
return gettext('There are no items in the table.') return gettext('There are no items in the table.')
# URL generation helper # URL generation helpers
def _get_extra_args(self): def _get_list_filter_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
if self._filters: if self._filters:
sfilters = [] filters = []
for n in request.args: for n in request.args:
if n.startswith('flt'): if not n.startswith('flt'):
ofs = n.find('_') continue
if ofs == -1:
continue
try: if '_' not in n:
pos = int(n[3:ofs]) continue
idx = int(n[ofs + 1:])
except ValueError:
continue
if idx >= 0 and idx < len(self._filters): pos, key = n[3:].split('_', 1)
flt = self._filters[idx]
value = request.args[n] if key in self._filter_args:
idx, flt = self._filter_args[key]
if flt.validate(value): value = request.args[n]
sfilters.append((pos, (idx, flt.clean(value))))
filters = [v[1] for v in sorted(sfilters, key=lambda n: n[0])] if flt.validate(value):
else: filters.append((pos, (idx, flt.clean(value))))
filters = None
# 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 return page, sort, sort_desc, search, filters
...@@ -1016,9 +1053,11 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1016,9 +1053,11 @@ class BaseModelView(BaseView, ActionsMixin):
kwargs = dict(page=page, sort=sort, desc=sort_desc, search=search) kwargs = dict(page=page, sort=sort, desc=sort_desc, search=search)
if filters: if filters:
for i, flt in enumerate(filters): for i, pair in enumerate(filters):
key = 'flt%d_%d' % (i, flt[0]) idx, value = pair
kwargs[key] = flt[1]
key = 'flt%d_%s' % (i, self.get_filter_arg(idx, self._filters[idx]))
kwargs[key] = value
return url_for(view, **kwargs) return url_for(view, **kwargs)
...@@ -1038,12 +1077,6 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1038,12 +1077,6 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
return rec_getattr(model, name) 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 @contextfunction
def get_list_value(self, context, model, name): def get_list_value(self, context, model, name):
""" """
...@@ -1104,7 +1137,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1104,7 +1137,7 @@ class BaseModelView(BaseView, ActionsMixin):
List view List view
""" """
# Grab parameters from URL # 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 # Map column index to column name
sort_column = self._get_column_by_idx(sort_idx) sort_column = self._get_column_by_idx(sort_idx)
...@@ -1120,18 +1153,6 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1120,18 +1153,6 @@ class BaseModelView(BaseView, ActionsMixin):
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
...@@ -1187,8 +1208,6 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1187,8 +1208,6 @@ class BaseModelView(BaseView, ActionsMixin):
# Filters # Filters
filters=self._filters, filters=self._filters,
filter_groups=self._filter_groups, filter_groups=self._filter_groups,
filter_types=self._filter_types,
filter_data=filters_data,
active_filters=filters, active_filters=filters,
# Actions # Actions
...@@ -1253,7 +1272,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1253,7 +1272,7 @@ class BaseModelView(BaseView, ActionsMixin):
return redirect(return_url) return redirect(return_url)
form_opts = FormOpts(widget_args=self.form_widget_args, 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, return self.render(self.edit_template,
model=model, model=model,
......
...@@ -102,10 +102,11 @@ class InlineModelFormField(FormField): ...@@ -102,10 +102,11 @@ class InlineModelFormField(FormField):
""" """
widget = InlineFormWidget() 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) super(InlineModelFormField, self).__init__(form_class, **kwargs)
self._pk = pk self._pk = pk
self.form_opts = form_opts
def get_pk(self): def get_pk(self):
return getattr(self.form, self._pk).data return getattr(self.form, self._pk).data
......
...@@ -12,12 +12,11 @@ class InlineFieldListWidget(RenderTemplateWidget): ...@@ -12,12 +12,11 @@ class InlineFieldListWidget(RenderTemplateWidget):
class InlineFormWidget(RenderTemplateWidget): class InlineFormWidget(RenderTemplateWidget):
def __init__(self, form_opts=None): def __init__(self):
super(InlineFormWidget, self).__init__('admin/model/inline_form.html') super(InlineFormWidget, self).__init__('admin/model/inline_form.html')
self.form_opts = form_opts
def __call__(self, field, **kwargs): 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) return super(InlineFormWidget, self).__call__(field, **kwargs)
......
...@@ -31,6 +31,10 @@ form.search-form .clear i { ...@@ -31,6 +31,10 @@ form.search-form .clear i {
margin: 2px 0 0 0; margin: 2px 0 0 0;
} }
form.search-form div input {
margin: 0;
}
/* Filters */ /* Filters */
table.filters { table.filters {
border-collapse: collapse; border-collapse: collapse;
...@@ -105,3 +109,7 @@ table.filters { ...@@ -105,3 +109,7 @@ table.filters {
.select2-results li { .select2-results li {
min-height: 24px !important; 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 $root = $(element);
var $container = $('.filters', $root); var $container = $('.filters', $root);
var lastCount = 0; var lastCount = 0;
function getCount(name) { function getCount(name) {
var idx = name.indexOf('_'); 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() { function changeOperation() {
...@@ -23,44 +34,44 @@ var AdminFilters = function(element, filters_element, operations, options, types ...@@ -23,44 +34,44 @@ var AdminFilters = function(element, filters_element, operations, options, types
return false; return false;
} }
function addFilter(name, op) { function addFilter(name, subfilters) {
var $el = $('<tr />').appendTo($container); var $el = $('<tr />').appendTo($container);
// Filter list // Filter list
$el.append( $el.append(
$('<td/>').append( $('<td/>').append(
$('<a href="#" class="btn remove-filter" />') $('<a href="#" class="btn remove-filter" />')
.append($('<span class="close-icon">&times;</span>')) .append($('<span class="close-icon">&times;</span>'))
.append('&nbsp;') .append('&nbsp;')
.append(name) .append(name)
.click(removeFilter) .click(removeFilter)
) )
); );
// Filter type // Filter type
var $select = $('<select class="filter-op" />') var $select = $('<select class="filter-op" />')
.change(changeOperation); .change(changeOperation);
$(op).each(function() { $(subfilters).each(function() {
$select.append($('<option/>').attr('value', this[0]).text(this[1])); $select.append($('<option/>').attr('value', this.arg).text(this.operation));
}); });
$el.append( $el.append(
$('<td/>').append($select) $('<td/>').append($select)
); );
$select.select2({width: 'resolve'}); $select.select2({width: 'resolve'});
// Input // Input
var optId = op[0][0]; var filter = subfilters[0];
var $field; var $field;
if (optId in options) { if (filter.options) {
$field = $('<select class="filter-val" />') $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/>') $field.append($('<option/>')
.val(this[0]).text(this[1])); .val(this[0]).text(this[1]));
}); });
...@@ -70,22 +81,20 @@ var AdminFilters = function(element, filters_element, operations, options, types ...@@ -70,22 +81,20 @@ var AdminFilters = function(element, filters_element, operations, options, types
} else } else
{ {
$field = $('<input type="text" class="filter-val" />') $field = $('<input type="text" class="filter-val" />')
.attr('name', 'flt' + lastCount + '_' + optId); .attr('name', makeName(filter.arg));
$el.append($('<td/>').append($field)); $el.append($('<td/>').append($field));
} }
if (optId in types) { if (filter.type) {
$field.attr('data-role', types[optId]); $field.attr('data-role', filter.type);
faForm.applyStyle($field, types[optId]); faForm.applyStyle($field, filter.type);
} }
lastCount += 1;
} }
$('a.filter', filters_element).click(function() { $('a.filter', filtersElement).click(function() {
var name = $(this).text().trim(); var name = $(this).text().trim();
addFilter(name, operations[name]); addFilter(name, filterGroups[name]);
$('button', $root).show(); $('button', $root).show();
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<ul class="dropdown-menu field-filters"> <ul class="dropdown-menu field-filters">
{% for k in filter_groups %} {% for k in filter_groups %}
<li> <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> </li>
{% endfor %} {% endfor %}
</ul> </ul>
...@@ -21,31 +21,32 @@ ...@@ -21,31 +21,32 @@
</div> </div>
<table class="filters"> <table class="filters">
{%- for i, flt in enumerate(active_filters) -%} {%- for n, values in enumerate(active_filters) -%}
<tr> <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> <td>
<a href="javascript:void(0)" class="btn remove-filter" title="{{ _gettext('Remove Filter') }}"> <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> </a>
</td> </td>
<td> <td>
<select class="filter-op" data-role="select2"> <select class="filter-op" data-role="select2">
{% for op in admin_view._filter_dict[filter.name] %} {% for op in filter_groups[filter.name] %}
<option value="{{ op[0] }}"{% if flt[0] == op[0] %} selected="selected"{% endif %}>{{ op[1] }}</option> <option value="{{ op['arg'] }}"{% if idx == op['index'] %} selected="selected"{% endif %}>{{ op['operation'] }}</option>
{% endfor %} {% endfor %}
</select> </select>
</td> </td>
<td> <td>
{%- set data = filter_data.get(flt[0]) -%} {%- if filter.options -%}
{%- if data -%} <select name="flt{{n}}_{{ filter_arg }}" class="filter-val" data-role="select2">
<select name="flt{{ i }}_{{ flt[0] }}" class="filter-val" data-role="select2"> {%- for d in filter.options %}
{%- for d in data %} <option value="{{ d[0] }}"{% if value == d[0] %} selected{% endif %}>{{ d[1] }}</option>
<option value="{{ d[0] }}"{% if flt[1] == d[0] %} selected{% endif %}>{{ d[1] }}</option>
{%- endfor %} {%- endfor %}
</select> </select>
{%- else -%} {%- 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 -%} {%- endif -%}
</td> </td>
</tr> </tr>
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
</li> </li>
{% endif %} {% endif %}
{% if filter_groups %} {% if filters %}
<li class="dropdown"> <li class="dropdown">
{{ model_layout.filter_options() }} {{ model_layout.filter_options() }}
</li> </li>
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
</ul> </ul>
{% endblock %} {% endblock %}
{% if filter_groups %} {% if filters %}
{{ model_layout.filter_form() }} {{ model_layout.filter_form() }}
<div class="clearfix"></div> <div class="clearfix"></div>
{% endif %} {% endif %}
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
<tr> <tr>
{% block list_header scoped %} {% block list_header scoped %}
{% if actions %} {% if actions %}
<th class="span1"> <th class="list-checkbox-column">
<input type="checkbox" name="rowtoggle" class="action-rowtoggle" title="{{ _gettext('Select all records') }}" /> <input type="checkbox" name="rowtoggle" class="action-rowtoggle" title="{{ _gettext('Select all records') }}" />
</th> </th>
{% endif %} {% endif %}
...@@ -159,12 +159,10 @@ ...@@ -159,12 +159,10 @@
html: true, html: true,
placement: 'bottom' placement: 'bottom'
}); });
{% if filter_groups is not none and filter_data is not none %} {% if filter_groups %}
var filter = new AdminFilters( var filter = new AdminFilters(
'#filter_form', '.field-filters', '#filter_form', '.field-filters',
{{ admin_view._get_filter_dict()|tojson|safe }}, {{ filter_groups|tojson|safe }}
{{ filter_data|tojson|safe }},
{{ filter_types|tojson|safe }}
); );
{% endif %} {% endif %}
})(jQuery); })(jQuery);
......
...@@ -233,61 +233,71 @@ def test_column_filters(): ...@@ -233,61 +233,71 @@ def test_column_filters():
eq_(len(view._filters), 4) eq_(len(view._filters), 4)
eq_(view._filter_dict, { eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Test1']],
u'Test1': [ [
(0, u'equals'), (0, u'equals'),
(1, u'not equal'), (1, u'not equal'),
(2, u'contains'), (2, u'contains'),
(3, u'not contains') (3, u'not contains')
]}) ])
# Test filter that references property # Test filter that references property
view = CustomModelView(Model2, db.session, view = CustomModelView(Model2, db.session,
column_filters=['model1']) column_filters=['model1'])
eq_(view._filter_dict, { eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Test1']],
u'Model1 / Test1': [ [
(0, u'equals'), (0, u'equals'),
(1, u'not equal'), (1, u'not equal'),
(2, u'contains'), (2, u'contains'),
(3, u'not contains') (3, u'not contains')
], ])
'Model1 / Test2': [
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Test2']],
[
(4, 'equals'), (4, 'equals'),
(5, 'not equal'), (5, 'not equal'),
(6, 'contains'), (6, 'contains'),
(7, 'not contains') (7, 'not contains')
], ])
u'Model1 / Test3': [
eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Test3']],
[
(8, u'equals'), (8, u'equals'),
(9, u'not equal'), (9, u'not equal'),
(10, u'contains'), (10, u'contains'),
(11, u'not 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'), (12, u'equals'),
(13, u'not equal'), (13, u'not equal'),
(14, u'contains'), (14, u'contains'),
(15, u'not 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'), (16, u'equals'),
(17, u'not equal'), (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'), (18, u'equals'),
(19, u'not equal'), (19, u'not equal'),
]}) ])
# Test filter with a dot # Test filter with a dot
view = CustomModelView(Model2, db.session, view = CustomModelView(Model2, db.session,
column_filters=['model1.bool_field']) column_filters=['model1.bool_field'])
eq_(view._filter_dict, { eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Model1 / Bool Field']],
'Model1 / Bool Field': [ [
(0, 'equals'), (0, 'equals'),
(1, 'not equal'), (1, 'not equal'),
]}) ])
# Fill DB # Fill DB
model1_obj1 = Model1('model1_obj1', bool_field=True) model1_obj1 = Model1('model1_obj1', bool_field=True)
...@@ -324,11 +334,15 @@ def test_column_filters(): ...@@ -324,11 +334,15 @@ def test_column_filters():
column_filters=['int_field']) column_filters=['int_field'])
admin.add_view(view) admin.add_view(view)
eq_(view._filter_dict, {'Int Field': [(0, 'equals'), (1, 'not equal'), eq_([(f['index'], f['operation']) for f in view._filter_groups[u'Int Field']],
(2, 'greater than'), (3, 'smaller than')]}) [
(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( view = CustomModelView(
Model2, db.session, Model2, db.session,
endpoint='_model2', endpoint='_model2',
...@@ -349,6 +363,21 @@ def test_column_filters(): ...@@ -349,6 +363,21 @@ def test_column_filters():
ok_('model2_obj3' not in data) ok_('model2_obj3' not in data)
ok_('model2_obj4' 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(): def test_url_args():
app, db, admin = setup() app, db, admin = setup()
......
...@@ -302,8 +302,8 @@ def test_column_filters(): ...@@ -302,8 +302,8 @@ def test_column_filters():
eq_(view._filters[0].name, 'col1') eq_(view._filters[0].name, 'col1')
eq_(view._filters[1].name, 'col2') eq_(view._filters[1].name, 'col2')
eq_(view._filter_dict, {'col1': [(0, 'test')], eq_([(f['index'], f['operation']) for f in view._filter_groups[u'col1']], [(0, 'test')])
'col2': [(1, 'test')]}) eq_([(f['index'], f['operation']) for f in view._filter_groups[u'col2']], [(1, 'test')])
# TODO: Make calls with filters # TODO: Make calls with filters
......
...@@ -189,6 +189,10 @@ msgstr "Уже существует." ...@@ -189,6 +189,10 @@ msgstr "Уже существует."
msgid "Model was successfully created." msgid "Model was successfully created."
msgstr "Запись была создана." msgstr "Запись была создана."
#: ../flask_admin/model/base.py:1268
msgid "Model was successfully saved."
msgstr "Запись была сохранена."
#: ../flask_admin/model/filters.py:82 #: ../flask_admin/model/filters.py:82
msgid "Yes" msgid "Yes"
msgstr "Да" 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