• 技术文章 >后端开发 >Python教程

    Flask的图形化管理界面搭建框架Flask-Admin的使用教程

    2016-06-16 08:47:50原创3485

    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('//m.sbmmt.com/m/')
      def index(self):
        return self.render('myadmin.html')
    
    
    class AnotherAdminView(admin.BaseView):
      @admin.expose('//m.sbmmt.com/m/')
      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('//m.sbmmt.com/m/')
    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) %}
       {% 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('//m.sbmmt.com/m/')
    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相对应。

    声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn核实处理。
    专题推荐:Flask Flask-Admin Python

    相关文章推荐

    • Python之os操作实例• 十个Python程序员易犯的错误• 详解Python中的条件判断语句• 使用Python中的greenlet包实现并发编程的入门教程• 浅谈Python的Django框架中的缓存控制
    1/1

    PHP中文网