Commit c07ed4b5 authored by Paul Brown's avatar Paul Brown

Merge pull request #1 from mrjoes/master

Merge most recent changes
parents f820bd9e 3bf7e239
......@@ -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
......
This diff is collapsed.
__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
......@@ -31,8 +31,8 @@ class User(db.Model):
username = db.Column(db.String(80), unique=True)
email = db.Column(db.String(120), unique=True)
# Required for administrative interface
def __str__(self):
# Required for administrative interface. For python 3 please use __str__ instead.
def __unicode__(self):
return self.username
......@@ -54,7 +54,7 @@ class Post(db.Model):
tags = db.relationship('Tag', secondary=post_tags_table)
def __str__(self):
def __unicode__(self):
return self.title
......@@ -62,7 +62,7 @@ class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64))
def __str__(self):
def __unicode__(self):
return self.name
......@@ -75,7 +75,7 @@ class UserInfo(db.Model):
user_id = db.Column(db.Integer(), db.ForeignKey(User.id))
user = db.relationship(User, backref='info')
def __str__(self):
def __unicode__(self):
return '%s - %s' % (self.key, self.value)
......@@ -85,7 +85,7 @@ class Tree(db.Model):
parent_id = db.Column(db.Integer, db.ForeignKey('tree.id'))
parent = db.relationship('Tree', remote_side=[id], backref='children')
def __str__(self):
def __unicode__(self):
return self.name
......
......@@ -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())
......@@ -155,6 +155,11 @@ class FileAdmin(BaseView, ActionsMixin):
Edit template
"""
upload_form = UploadForm
"""
Upload form class
"""
def __init__(self, base_path, base_url=None,
name=None, category=None, endpoint=None, url=None,
verify_path=True):
......@@ -285,7 +290,7 @@ class FileAdmin(BaseView, ActionsMixin):
"""
file_data.save(path)
def _get_dir_url(self, endpoint, path, **kwargs):
def _get_dir_url(self, endpoint, path=None, **kwargs):
"""
Return prettified URL
......@@ -410,6 +415,17 @@ class FileAdmin(BaseView, ActionsMixin):
"""
pass
def _save_form_files(self, directory, path, form):
filename = op.join(directory,
secure_filename(form.upload.data.filename))
if op.exists(filename):
flash(gettext('File "%(name)s" already exists.', name=filename),
'error')
else:
self.save_file(filename, form.upload.data)
self.on_file_upload(directory, path, filename)
@expose('/')
@expose('/b/<path:path>')
def index(self, path=None):
......@@ -423,7 +439,7 @@ class FileAdmin(BaseView, ActionsMixin):
base_path, directory, path = self._normalize_path(path)
if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.')))
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
# Get directory listing
......@@ -486,24 +502,16 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(self._get_dir_url('.index', path))
if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.')))
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
form = UploadForm(self)
form = self.upload_form(self)
if helpers.validate_form_on_submit(form):
filename = op.join(directory,
secure_filename(form.upload.data.filename))
if op.exists(filename):
flash(gettext('File "%(name)s" already exists.', name=filename),
'error')
else:
try:
self.save_file(filename, form.upload.data)
self.on_file_upload(directory, path, filename)
return redirect(self._get_dir_url('.index', path))
except Exception as ex:
flash(gettext('Failed to save file: %(error)s', error=ex))
try:
self._save_form_files(directory, path, form)
return redirect(self._get_dir_url('.index', path))
except Exception as ex:
flash(gettext('Failed to save file: %(error)s', error=ex))
return self.render(self.upload_template, form=form)
......@@ -547,7 +555,7 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(dir_url)
if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.')))
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
form = NameForm(helpers.get_form_data())
......@@ -558,7 +566,7 @@ class FileAdmin(BaseView, ActionsMixin):
self.on_mkdir(directory, form.name.data)
return redirect(dir_url)
except Exception as ex:
flash(gettext('Failed to create directory: %(error)s', ex), 'error')
flash(gettext('Failed to create directory: %(error)s', error=ex), 'error')
return self.render(self.mkdir_template,
form=form,
......@@ -584,7 +592,7 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(return_url)
if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.')))
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
if op.isdir(full_path):
......@@ -627,7 +635,7 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(return_url)
if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.')))
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
if not op.exists(full_path):
......@@ -672,8 +680,8 @@ class FileAdmin(BaseView, ActionsMixin):
base_path, full_path, path = self._normalize_path(path)
if not self.is_accessible_path(path):
flash(gettext(gettext('Permission denied.')))
if not self.is_accessible_path(path) or not self.is_file_editable(path):
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
dir_url = self._get_dir_url('.index', os.path.dirname(path))
......
......@@ -106,9 +106,9 @@ class AdminModelConverter(ModelConverterBase):
kwargs['label'] = self._get_label(prop.key, kwargs)
kwargs['description'] = self._get_description(prop.key, kwargs)
if column.nullable:
if column.nullable or prop.direction.name != 'MANYTOONE':
kwargs['validators'].append(validators.Optional())
elif prop.direction.name != 'MANYTOMANY':
else:
kwargs['validators'].append(validators.InputRequired())
# Contribute model-related parameters
......
......@@ -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):
......
......@@ -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)
......@@ -428,7 +429,10 @@ class ModelView(BaseModelView):
:returns:
``True`` for ``String``, ``Unicode``, ``Text``, ``UnicodeText``
"""
return name in ('String', 'Unicode', 'Text', 'UnicodeText')
if name:
name = name.lower()
return name in ('string', 'unicode', 'text', 'unicodetext')
def scaffold_filters(self, name):
"""
......
......@@ -177,7 +177,8 @@ class FileUploadField(fields.TextField):
return True
return ('.' in filename and
filename.rsplit('.', 1)[1] in self.allowed_extensions)
filename.rsplit('.', 1)[1].lower() in
map(str.lower, self.allowed_extensions))
def pre_validate(self, form):
if (self.data and
......@@ -208,6 +209,8 @@ class FileUploadField(fields.TextField):
filename = self.generate_name(obj, self.data)
filename = self._save_file(self.data, filename)
# update filename of FileStorage to our validated name
self.data.filename = filename
setattr(obj, name, filename)
......@@ -329,7 +332,7 @@ class ImageUploadField(FileUploadField):
"""
# Check if PIL is installed
if Image is None:
raise Exception('PIL library was not found')
raise ImportError('PIL library was not found')
self.max_size = max_size
self.thumbnail_fn = thumbgen or thumbgen_filename
......
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):
"""
......@@ -1058,9 +1091,9 @@ class BaseModelView(BaseView, ActionsMixin):
"""
column_fmt = self.column_formatters.get(name)
if column_fmt is not None:
return column_fmt(self, context, model, name)
value = self._get_field_value(model, name)
value = column_fmt(self, context, model, name)
else:
value = self._get_field_value(model, name)
choices_map = self._column_choices_map.get(name, {})
if choices_map:
......@@ -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
......
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();
......
......@@ -90,7 +90,7 @@
{% endif %}
</td>
<td>
{{ size }}
{{ size|filesizeformat }}
</td>
{% endif %}
{% endblock %}
......
......@@ -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 %}
......@@ -99,6 +99,7 @@
<input type="checkbox" name="rowid" class="action-checkbox" value="{{ get_pk_value(row) }}" title="{{ _gettext('Select record') }}" />
</td>
{% endif %}
{% block list_row_actions_column scoped %}
<td>
{% block list_row_actions scoped %}
{%- if admin_view.can_edit -%}
......@@ -118,6 +119,7 @@
{%- endif -%}
{% endblock %}
</td>
{% endblock %}
{% for c, name in list_columns %}
<td>{{ get_value(row, c) }}</td>
{% endfor %}
......@@ -157,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()
......
......@@ -95,6 +95,10 @@ def test_image_upload_field():
safe_delete(path, 'test2.png')
safe_delete(path, 'test2_thumb.jpg')
safe_delete(path, 'test1.jpg')
safe_delete(path, 'test1.jpeg')
safe_delete(path, 'test1.gif')
safe_delete(path, 'test1.png')
safe_delete(path, 'test1.tiff')
class TestForm(form.BaseForm):
upload = form.ImageUploadField('Upload',
......@@ -204,6 +208,25 @@ def test_image_upload_field():
ok_(op.exists(op.join(path, 'test1.jpg')))
# check allowed extensions
for extension in ('gif', 'jpg', 'jpeg', 'png', 'tiff'):
filename = 'copyleft.' + extension
filepath = op.join(op.dirname(__file__), 'data', filename)
with open(filepath, 'rb') as fp:
with app.test_request_context(method='POST', data={'upload': (fp, filename)}):
my_form = TestNoResizeForm(helpers.get_form_data())
ok_(my_form.validate())
my_form.populate_obj(dummy)
eq_(dummy.upload, my_form.upload.data.filename)
# check case-sensitivity for extensions
filename = op.join(op.dirname(__file__), 'data', 'copyleft.jpg')
with open(filename, 'rb') as fp:
with app.test_request_context(method='POST', data={'upload': (fp, 'copyleft.JPG')}):
my_form = TestNoResizeForm(helpers.get_form_data())
ok_(my_form.validate())
def test_relative_path():
app = Flask(__name__)
......
......@@ -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
......
......@@ -254,6 +254,10 @@ msgstr "Создать"
msgid "Save and Add"
msgstr "Сохранить и Добавить"
#: ../flask_admin/templates/admin/model/edit.html:6
msgid "Save and Continue"
msgstr "Сохранить и Продолжить"
#: ../flask_admin/templates/admin/model/inline_form_list.html:24
msgid "Delete?"
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