A LITTLE BIT MORE FAST,A LITTLE BIT MORE STRONG。
工作中要提供一个简易的可视化web界面供用户(开发者?)方便地查看数据库的内容,可以的话最好还能对数据库进行增删改查,flask-Admin就是我想要的!
Flask-Admin是什么?
Flask-Admin是Flask框架的一个扩展,用它能够快速创建Web管理界面,它实现了比如用户、文件的增删改查等常用的管理功能;如果对它的默认界面不喜欢,可以通过修改模板文件来定制;
Flask-Admin把每一个菜单(超链接)看作一个view,注册后才能显示出来,view本身也有属性来控制其是否可见;因此,利用这个机制可以定制自己的模块化界面,比如让不同权限的用户登录后看到不一样的菜单;
如何使用
Flask-Admin的文档写得比较简单,单从文档很难理解如何使用,好在作者提供了的很多Example(见 源码 ),基本上照葫芦画瓢即可;
下面就通过分析一些Example代码来学习使用
example/simple
这是最简单的一个样例,可以帮助我们快速、直观的了解基本概念,学会定制Flask-Admin的界面
simple.py
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41from flask import Flask
from flask.ext import admin
# Create custom admin view
class MyAdminView(admin.BaseView):
def index(self):
return self.render('myadmin.html')
class AnotherAdminView(admin.BaseView):
def index(self):
return self.render('anotheradmin.html')
def test(self):
return self.render('test.html')
# Create flask app
app = Flask(__name__, template_folder='templates')
app.debug = True
# Flask views
def index():
return 'Click me to get to Admin!'
# Create admin interface
admin = admin.Admin()
admin.add_view(MyAdminView(category='Test'))
admin.add_view(AnotherAdminView(category='Test'))
admin.init_app(app)
if __name__ == '__main__':
# Start app
app.run()
BaseView
所有的view都必须继承自 BaseView :
1 | class BaseView(name=None, category=None, endpoint=None, url=None, static_folder=None, static_url_path=None) |
name: view在页面上表现为一个menu(超链接),menu name == ‘name’,缺省就用小写的class name
category: 如果多个view有相同的category就全部放到一个dropdown里面(dropdown name==’category’)
endpoint: 假设endpoint=’xxx’,则可以用 url_for(xxx.index)
,也能改变页面URL( /admin/xxx
)
url: 页面URL,优先级 url > endpoint > class name
static_folder: static目录的路径
static_url_path: static目录的URLanotheradmin.html
:1
2
3
4
5
6{% extends 'admin/master.html' %}
{% block body %}
Hello World from AnotherMyAdmin!
Click me to go to test view
{% endblock %}
如果 AnotherAdminView
增加参数 endpoint='xxx'
,那这里就可以写成 url_for('xxx.text')
,然后页面URL会由 /admin/anotheradminview/
变成 /admin/xxx
如果同时指定参数 url='aaa'
,那页面URL会变成 /admin/aaa
,url优先级比endpoint高
Admin
1 | class Admin(app=None, name=None, url=None, subdomain=None, index_view=None, translations_path=None, endpoint=None, static_url_path=None, base_template=None) |
app: Flask Application Object;本例中可以不写 admin.init_app(app)
,直接用 admin = admin.Admin(app=app)
是一样的
name: Application name,缺省’Admin’;会显示为main menu name(‘Home’左边的’Admin’)和page title
subdomain: ???
index_view: ‘Home’那个menu对应的就叫index view,缺省 AdminIndexView
base_template: 基础模板,缺省 admin/base.html
,该模板在Flask-Admin的源码目录里面
部分Admin代码如下:
1 | class MenuItem(object): |
从上面的代码可以看出 init_app(app)
和 Admin(app=app)
是一样的:
将每个view注册为blueprint(Flask里的概念,可以简单理解为模块)
记录所有view,以及所属的category和url
AdminIndexView1
class AdminIndexView(name=None, category=None, endpoint=None, url=None, template='admin/index.html')
-name: 缺省’Home’
-endpoint: 缺省’admin’
-url: 缺省’/admin’
如果要封装出自己的view,可以参照 AdminIndexView
的写法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class AdminIndexView(BaseView):
def __init__(self, name=None, category=None,
endpoint=None, url=None,
template='admin/index.html'):
super(AdminIndexView, self).__init__(name or babel.lazy_gettext('Home'),
category,
endpoint or 'admin',
url or '/admin',
'static')
self._template = template
def index(self):
return self.render(self._template)
base_template
base_template缺省是 /admin/base.html
,是页面的主要代码(基于 bootstrap ),它里面又import admin/layout.html
;
layout是一些宏,主要用于展开、显示menu;
在模板中使用一些变量来取出之前注册view时保存的信息(如menu name和url等):
1 | # admin/layout.html (部分) |
example/file
这个样例能帮助我们快速搭建起文件管理界面,但我们的重点是学习使用 ActionsMixin
模块
file.py
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35import os
import os.path as op
from flask import Flask
from flask.ext import admin
from flask.ext.admin.contrib import fileadmin
# Create flask app
app = Flask(__name__, template_folder='templates', static_folder='files')
# Create dummy secrey key so we can use flash
app.config['SECRET_KEY'] = '123456790'
# Flask views
def index():
return 'Click me to get to Admin!'
if __name__ == '__main__':
# Create directory
path = op.join(op.dirname(__file__), 'files')
try:
os.mkdir(path)
except OSError:
pass
# Create admin interface
admin = admin.Admin(app)
admin.add_view(fileadmin.FileAdmin(path, '/files/', name='Files'))
# Start app
app.run(debug=True)
FileAdmin是已经写好的的一个view,直接用即可:1
class FileAdmin(base_path, base_url, name=None, category=None, endpoint=None, url=None, verify_path=True)
-base_path: 文件存放的相对路径
-base_url: 文件目录的URL
FileAdmin中和ActionsMixin相关代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34class FileAdmin(BaseView, ActionsMixin):
def __init__(self, base_path, base_url,
name=None, category=None, endpoint=None, url=None,
verify_path=True):
self.init_actions()
def action_view(self):
return self.handle_action()
# Actions
lazy_gettext('Delete'),
lazy_gettext('Are you sure you want to delete these files?'))
def action_delete(self, items):
if not self.can_delete:
flash(gettext('File deletion is disabled.'), 'error')
return
for path in items:
base_path, full_path, path = self._normalize_path(path)
if self.is_accessible_path(path):
try:
os.remove(full_path)
flash(gettext('File "%(name)s" was successfully deleted.', name=path))
except Exception as ex:
flash(gettext('Failed to delete file: %(name)s', name=ex), 'error')
def action_edit(self, items):
return redirect(url_for('.edit', path=items))
@action()
用于wrap跟在后面的函数,这里的作用就是把参数保存起来:1
2
3
4
5
6def action(name, text, confirmation=None)
def wrap(f):
f._action = (name, text, confirmation)
return f
return wrap
-name: action name
-text: 可用于按钮名称
-confirmation: 弹框确认信息init_actions()
把所有action的信息保存到 ActionsMixin
里面:
1 | # 调试信息 |
action_view()
用于处理POST给 /action/
的请求,然后调用 handle_action()
,它再调用不同的action处理,最后返回当前页面:
1 | # 省略无关代码 |
ids是一个文件清单,作为参数传给action处理函数(参数items):1
2# 调试信息
ids: [u'1.png', u'2.png']
再分析页面代码, Files
页面对应文件为 admin/file/list.html
,重点看 With selected
下拉菜单相关代码:
1 | {% import 'admin/actions.html' as actionslib with context %} |
上面用到的三个宏在 actions.html
:
1 | {% macro dropdown(actions, btn_class='dropdown-toggle') -%} |
选择菜单后的处理方法在 actions.js
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36var AdminModelActions = function(actionErrorMessage, actionConfirmations) {
// Actions helpers. TODO: Move to separate file
this.execute = function(name) {
var selected = $('input.action-checkbox:checked').size();
if (selected === 0) {
alert(actionErrorMessage);
return false;
}
var msg = actionConfirmations[name];
if (!!msg)
if (!confirm(msg))
return false;
// Update hidden form and submit it
var form = $('#action_form');
$('#action', form).val(name);
$('input.action-checkbox', form).remove();
$('input.action-checkbox:checked').each(function() {
form.append($(this).clone());
});
form.submit();
return false;
};
$(function() {
$('.action-rowtoggle').change(function() {
$('input.action-checkbox').attr('checked', this.checked);
});
});
};
对比一下修改前后的表单:1
2
3
4
5
6
7
总结一下,当我们点击下拉菜单中的菜单项(Delete,Edit),本地JavaScript代码会弹出确认框(假设有确认信息),然后提交一个表单给 /admin/fileadmin/action/
,请求处理函数 action_view()
根据表单类型再调用不同的action处理函数,最后返回一个页面。