Flask-Admin是Flask框架的一个扩展,用它能够快速创建Web管理界面,它实现了比如用户、文件的增删改查等常用的管理功能;如果对它的默认界面不喜欢,可以通过修改模板文件来定制;
Flask-Admin把每一个菜单(超链接)看作一个view,注册后才能显示出来,view本身也有属性来控制其是否可见;因此,利用这个机制可以定制自己的模块化界面,比如让不同权限的用户登录后看到不一样的菜单;
项目地址:https://flask-admin.readthedocs.io/en/latest/
example/simple
这是最简单的一个样例,可以帮助我们快速、直观的了解基本概念,学会定制Flask-Admin的界面
simple.py:
from flask import Flask from flask.ext import admin # Create custom admin view class MyAdminView(admin.BaseView): @admin.expose(\'/\') def index(self): return self.render(\'myadmin.html\') class AnotherAdminView(admin.BaseView): @admin.expose(\'/\') def index(self): return self.render(\'anotheradmin.html\') @admin.expose(\'/test/\') def test(self): return self.render(\'test.html\') # Create flask app app = Flask(__name__, template_folder=\'templates\') app.debug = True # Flask views @app.route(\'/\') 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:
复制代码 代码如下:
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目录的URL
anotheradmin.html:
{% 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
复制代码 代码如下:
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代码如下:
class MenuItem(object): \"\"\" Simple menu tree hierarchy. \"\"\" def __init__(self, name, view=None): self.name = name self._view = view self._children = [] self._children_urls = set() self._cached_url = None self.url = None if view is not None: self.url = view.url def add_child(self, view): self._children.append(view) self._children_urls.add(view.url) class Admin(object): def __init__(self, app=None, name=None, url=None, subdomain=None, index_view=None, translations_path=None, endpoint=None, static_url_path=None, base_template=None): self.app = app self.translations_path = translations_path self._views = [] self._menu = [] self._menu_categories = dict() self._menu_links = [] if name is None: name = \'Admin\' self.name = name self.index_view = index_view or AdminIndexView(endpoint=endpoint, url=url) self.endpoint = endpoint or self.index_view.endpoint self.url = url or self.index_view.url self.static_url_path = static_url_path self.subdomain = subdomain self.base_template = base_template or \'admin/base.html\' # Add predefined index view self.add_view(self.index_view) # Register with application if app is not None: self._init_extension() def add_view(self, view): # Add to views self._views.append(view) # If app was provided in constructor, register view with Flask app if self.app is not None: self.app.register_blueprint(view.create_blueprint(self)) self._add_view_to_menu(view) def _add_view_to_menu(self, view): if view.category: category = self._menu_categories.get(view.category) if category is None: category = MenuItem(view.category) self._menu_categories[view.category] = category self._menu.append(category) category.add_child(MenuItem(view.name, view)) else: self._menu.append(MenuItem(view.name, view)) def init_app(self, app): self.app = app self._init_extension() # Register views for view in self._views: app.register_blueprint(view.create_blueprint(self)) self._add_view_to_menu(view)
从上面的代码可以看出init_app(app)和Admin(app=app)是一样的:
将每个view注册为blueprint(Flask里的概念,可以简单理解为模块)
记录所有view,以及所属的category和url
AdminIndexView
复制代码 代码如下:
class AdminIndexView(name=None, category=None, endpoint=None, url=None, template=\’admin/index.html\’)
name: 缺省\’Home\’
endpoint: 缺省\’admin\’
url: 缺省\’/admin\’
如果要封装出自己的view,可以参照AdminIndexView的写法:
class 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 @expose() 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等):
# admin/layout.html (部分)
{% macro menu() %} {% for item in admin_view.admin.menu() %} {% if item.is_category() %} {% set children = item.get_children() %} {% if children %} {% if item.is_active(admin_view) %}
example/file
这个样例能帮助我们快速搭建起文件管理界面,但我们的重点是学习使用ActionsMixin模块
file.py:
import 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 @app.route(\'/\') 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,直接用即可:
复制代码 代码如下:
class FileAdmin(base_path, base_url, name=None, category=None, endpoint=None, url=None, verify_path=True)
base_path: 文件存放的相对路径
base_url: 文件目录的URL
FileAdmin中和ActionsMixin相关代码如下:
class FileAdmin(BaseView, ActionsMixin):
def __init__(self, base_path, base_url, name=None, category=None, endpoint=None, url=None, verify_path=True): self.init_actions() @expose(\'/action/\', methods=(\'POST\',)) def action_view(self): return self.handle_action() # Actions @action(\'delete\', 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\') @action(\'edit\', lazy_gettext(\'Edit\')) def action_edit(self, items): return redirect(url_for(\'.edit\', path=items)) @action()用于wrap跟在后面的函数,这里的作用就是把参数保存起来: def 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里面:
# 调试信息 _actions = [(\'delete\', lu\'Delete\'), (\'edit\', lu\'Edit\')] _actions_data = {\'edit\': (>, lu\'Edit\', None), \'delete\': ( >, lu\'Delete\', lu\'Are you sure you want to delete these files?\')}
action_view()用于处理POST给/action/的请求,然后调用handle_action(),它再调用不同的action处理,最后返回当前页面:
# 省略无关代码 def handle_action(self, return_view=None): action = request.form.get(\'action\') ids = request.form.getlist(\'rowid\') handler = self._actions_data.get(action) if handler and self.is_action_allowed(action): response = handler[0](ids) if response is not None: return response if not return_view: url = url_for(\'.\' + self._default_view) else: url = url_for(\'.\' + return_view) return redirect(url)
ids是一个文件清单,作为参数传给action处理函数(参数items):
# 调试信息 ids: [u\'1.png\', u\'2.png\']
再分析页面代码,Files页面对应文件为admin/file/list.html,重点看With selected下拉菜单相关代码:
{% import \’admin/actions.html\’ as actionslib with context %}
{% if actions %}{{ actionslib.dropdown(actions, \'dropdown-toggle btn btn-large\') }}{% endif %} {% block actions %} {{ actionslib.form(actions, url_for(\'.action_view\')) }} {% endblock %} {% block tail %} {{ actionslib.script(_gettext(\'Please select at least one file.\'), actions, actions_confirmation) }} {% endblock %}
上面用到的三个宏在actions.html:
{% macro dropdown(actions, btn_class=\'dropdown-toggle\') -%} {{ _gettext(\'With selected\') }}{% endmacro %} {% macro form(actions, url) %} {% if actions %} {% endif %} {% endmacro %} {% macro script(message, actions, actions_confirmation) %} {% if actions %} {% endif %} {% endmacro %}
最终生成的页面(部分):
选择菜单后的处理方法在actions.js:
var 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); }); }); };
对比一下修改前后的表单:
# 初始化# \'Delete\'选中的三个文件 # \'Edit\'选中的一个文件
总结一下,当我们点击下拉菜单中的菜单项(Delete,Edit),本地JavaScript代码会弹出确认框(假设有确认信息),然后提交一个表单给/admin/fileadmin/action/,请求处理函数action_view()根据表单类型再调用不同的action处理函数,最后返回一个页面。
Flask-Admin字段(列)格式化
在某些情况下,我们需要对模型的某个属性进行格式化。比如,默认情况下,日期时间显示出来会比较长,这时可能需要只显示月和日,这时候,列格式化就派上用场了。
比如,如果你要显示双倍的价格,你可以这样做:
class MyModelView(BaseModelView): column_formatters = dict(price=lambda v, c, m, p: m.price*2)
或者在Jinja2模板中使用宏:
from flask.ext.admin.model.template import macro class MyModelView(BaseModelView): column_formatters = dict(price=macro(\'render_price\')) # in template {% macro render_price(model, column) %} {{ model.price * 2 }} {% endmacro %}
回调函数模型:
def formatter(view, context, model, name): # `view` is current administrative view # `context` is instance of jinja2.runtime.Context # `model` is model instance # `name` is property name pass
正好和上面的v, c, m, p相对应。