当前位置: 首页 > 工具软件 > Flask-Select2 > 使用案例 >

python flask后台框架_Flask的图形化管理界面搭建框架Flask-Admin的使用教程

贺浩漫
2023-12-01

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) %}

{% else %}{% endif %}

{{ item.name }}

{% for child in children %}

{% if child.is_active(admin_view) %}

{% else %}{% endif %}

{{ child.name }}

{% endfor %}

{% endif %}

{% else %}

{% if item.is_accessible() and item.is_visible() %}

{% if item.is_active(admin_view) %}

{% else %}{% endif %}

{{ item.name }}

{% endif %}

{% endif %}

{% endfor %}

{% endmacro %}

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') }}

{% for p in actions %}

{{ _gettext(p[1]) }}

{% endfor %}

{% endmacro %}

{% macro form(actions, url) %}

{% if actions %}

{% if csrf_token %}

{% endif %}

{% endif %}

{% endmacro %}

{% macro script(message, actions, actions_confirmation) %}

{% if actions %}

var modelActions = new AdminModelActions({{ message|tojson|safe }}, {{ actions_confirmation|tojson|safe }});

{% endif %}

{% endmacro %}

最终生成的页面(部分):

With selected

var modelActions = new AdminModelActions("Please select at least one file.", {"delete": "Are you sure you want to delete these files?"});

选择菜单后的处理方法在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相对应。

 类似资料: