Flask的圖形化管理界面搭建框架Flask-Admin的使用教程
Flask-Admin是Flask框架的一個擴展,用它能夠快速創(chuàng)建Web管理界面,它實現(xiàn)了比如用戶、文件的增刪改查等常用的管理功能;如果對它的默認界面不喜歡,可以通過修改模板文件來定制;
Flask-Admin把每一個菜單(超鏈接)看作一個view,注冊后才能顯示出來,view本身也有屬性來控制其是否可見;因此,利用這個機制可以定制自己的模塊化界面,比如讓不同權(quán)限的用戶登錄后看到不一樣的菜單;
項目地址:https://flask-admin.readthedocs.io/en/latest/
example/simple
這是最簡單的一個樣例,可以幫助我們快速、直觀的了解基本概念,學(xué)會定制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 '<a href="/admin/">Click me to get to Admin!</a>' # 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
class BaseView(name=None, category=None, endpoint=None, url=None, static_folder=None, static_url_path=None)
name: view在頁面上表現(xiàn)為一個menu(超鏈接),menu name == 'name',缺省就用小寫的class name
category: 如果多個view有相同的category就全部放到一個dropdown里面(dropdown name=='category')
endpoint: 假設(shè)endpoint='xxx',則可以用url_for(xxx.index),也能改變頁面URL(/admin/xxx)
url: 頁面URL,優(yōu)先級url > endpoint > class name
static_folder: static目錄的路徑
static_url_path: static目錄的URL
anotheradmin.html:
{% extends 'admin/master.html' %} {% block body %} Hello World from AnotherMyAdmin!<br/> <a href="{{ url_for('.test') }}">Click me to go to test view</a> {% endblock %}
如果AnotherAdminView增加參數(shù)endpoint='xxx',那這里就可以寫成url_for('xxx.text'),然后頁面URL會由/admin/anotheradminview/變成/admin/xxx
如果同時指定參數(shù)url='aaa',那頁面URL會變成/admin/aaa,url優(yōu)先級比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)
name: Application name,缺省'Admin';會顯示為main menu name('Home'左邊的'Admin')和page title
subdomain: ???
index_view: 'Home'那個menu對應(yīng)的就叫index view,缺省AdminIndexView
base_template: 基礎(chǔ)模板,缺省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) %}<li class="active dropdown">{% else %}<li class="dropdown">{% endif %} <a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">{{ item.name }}<b class="caret"></b></a> <ul class="dropdown-menu"> {% for child in children %} {% if child.is_active(admin_view) %}<li class="active">{% else %}<li>{% endif %} <a href="{{ child.get_url() }}">{{ child.name }}</a> </li> {% endfor %} </ul> </li> {% endif %} {% else %} {% if item.is_accessible() and item.is_visible() %} {% if item.is_active(admin_view) %}<li class="active">{% else %}<li>{% endif %} <a href="{{ item.get_url() }}">{{ item.name }}</a> </li> {% endif %} {% endif %} {% endfor %} {% endmacro %}
example/file
這個樣例能幫助我們快速搭建起文件管理界面,但我們的重點是學(xué)習(xí)使用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 '<a href="/admin/">Click me to get to Admin!</a>' 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是已經(jīng)寫好的的一個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相關(guān)代碼如下:
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跟在后面的函數(shù),這里的作用就是把參數(shù)保存起來: 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里面:
# 調(diào)試信息 _actions = [('delete', lu'Delete'), ('edit', lu'Edit')] _actions_data = {'edit': (<bound method FileAdmin.action_edit of <flask_admin.contrib.fileadmin.FileAdmin object at 0x1aafc50>>, lu'Edit', None), 'delete': (<bound method FileAdmin.action_delete of <flask_admin.contrib.fileadmin.FileAdmin object at 0x1aafc50>>, lu'Delete', lu'Are you sure you want to delete these files?')}
action_view()用于處理POST給/action/的請求,然后調(diào)用handle_action(),它再調(diào)用不同的action處理,最后返回當(dāng)前頁面:
# 省略無關(guān)代碼 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是一個文件清單,作為參數(shù)傳給action處理函數(shù)(參數(shù)items):
# 調(diào)試信息 ids: [u'1.png', u'2.png']
再分析頁面代碼,F(xiàn)iles頁面對應(yīng)文件為admin/file/list.html,重點看With selected下拉菜單相關(guān)代碼:
{% import 'admin/actions.html' as actionslib with context %}
{% if actions %} <div class="btn-group"> {{ actionslib.dropdown(actions, 'dropdown-toggle btn btn-large') }} </div> {% 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') -%} <a class="{{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)">{{ _gettext('With selected') }}<b class="caret"></b></a> <ul class="dropdown-menu"> {% for p in actions %} <li> <a href="javascript:void(0)" onclick="return modelActions.execute('{{ p[0] }}');">{{ _gettext(p[1]) }}</a> </li> {% endfor %} </ul> {% endmacro %} {% macro form(actions, url) %} {% if actions %} <form id="action_form" action="{{ url }}" method="POST" style="display: none"> {% if csrf_token %} <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> {% endif %} <input type="hidden" id="action" name="action" /> </form> {% endif %} {% endmacro %} {% macro script(message, actions, actions_confirmation) %} {% if actions %} <script src="{{ admin_static.url(filename='admin/js/actions.js') }}"></script> <script language="javascript"> var modelActions = new AdminModelActions({{ message|tojson|safe }}, {{ actions_confirmation|tojson|safe }}); </script> {% endif %} {% endmacro %}
最終生成的頁面(部分):
<div class="btn-group"> <a class="dropdown-toggle btn btn-large" href="javascript:void(0)" data-toggle="dropdown"> With selected <b class="caret"></b> </a> <ul class="dropdown-menu"> <li> <a onclick="return modelActions.execute('delete');" href="javascript:void(0)">Delete</a> </li> <li> <a onclick="return modelActions.execute('edit');" href="javascript:void(0)">Edit</a> </li> </ul> </div> <form id="action_form" action="/admin/fileadmin/action/" method="POST" style="display: none"> <input type="hidden" id="action" name="action" /> </form> <script src="/admin/static/admin/js/actions.js"></script> <script language="javascript"> var modelActions = new AdminModelActions("Please select at least one file.", {"delete": "Are you sure you want to delete these files?"}); </script>
選擇菜單后的處理方法在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); }); }); };
對比一下修改前后的表單:
# 初始化 <form id="action_form" style="display: none" method="POST" action="/admin/fileadmin/action/"> <input id="action" type="hidden" name="action"> </form> # 'Delete'選中的三個文件 <form id="action_form" style="display: none" method="POST" action="/admin/fileadmin/action/"> <input id="action" type="hidden" name="action" value="delete"> <input class="action-checkbox" type="checkbox" value="1.png" name="rowid"> <input class="action-checkbox" type="checkbox" value="2.png" name="rowid"> <input class="action-checkbox" type="checkbox" value="3.png" name="rowid"> </form> # 'Edit'選中的一個文件 <form id="action_form" style="display: none" method="POST" action="/admin/fileadmin/action/"> <input id="action" type="hidden" name="action" value="edit"> <input class="action-checkbox" type="checkbox" value="1.png" name="rowid"> </form>
總結(jié)一下,當(dāng)我們點擊下拉菜單中的菜單項(Delete,Edit),本地JavaScript代碼會彈出確認框(假設(shè)有確認信息),然后提交一個表單給/admin/fileadmin/action/,請求處理函數(shù)action_view()根據(jù)表單類型再調(diào)用不同的action處理函數(shù),最后返回一個頁面。
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 %}
回調(diào)函數(shù)模型:
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相對應(yīng)。
相關(guān)文章
python中小數(shù)點后取2位(四舍五入)及取2位(四舍五不入)的方法
這篇文章主要給大家介紹了python中小數(shù)點后取2位(四舍五入)及取2位(四舍五不入)的方法,在Python中取兩位小數(shù)的方法其實非常簡單,需要的朋友可以參考下2023-08-08Python return語句如何實現(xiàn)結(jié)果返回調(diào)用
這篇文章主要介紹了Python return語句如何實現(xiàn)結(jié)果返回調(diào)用,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-10-10scrapy爬蟲:scrapy.FormRequest中formdata參數(shù)詳解
這篇文章主要介紹了scrapy爬蟲:scrapy.FormRequest中formdata參數(shù)詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-04-04python常用數(shù)據(jù)重復(fù)項處理方法
在本篇文章里小編給大家整理的是關(guān)于python常用數(shù)據(jù)重復(fù)項處理方法,需要的朋友們參考下。2019-11-11利用Python自動監(jiān)控網(wǎng)站并發(fā)送郵件告警的方法
這篇文章介紹的是通過定時執(zhí)行python腳本,可以實現(xiàn)定期批量訪問網(wǎng)站,如果發(fā)現(xiàn)網(wǎng)站打不開,第一時間發(fā)郵件到管理員郵箱進行預(yù)警。有需要的可以參考借鑒。2016-08-08