Vue 2.0 起步(6) 后台管理Flask-Admin - 微信公众号RSS

1字数 1500阅读 7284

上一篇:Vue 2.0 起步(5) 订阅列表上传和下载 - 微信公众号RSS
本篇关键字:Flask-Admin 权限管理 定制显示格式 显示关系表外键内容 显示多对多(M2M)内容

Flask-Admin

Flask-Admin是一个功能齐全、简单易用的Flask扩展,让你为Flask应用程序增加管理界面。它受django-admin包的影响,开箱就有所有管理功能!但开发者拥有最终应用程序的外观、感觉和功能的全部控制权。
官网Link

Flask-Admin logo.png

本篇完成功能:

  1. 对模型model,有CRUD基本功能:
    CRUD就是Create、Read、Update、Delete
  2. 显示数据库内容,包括关系表Relation对应的内容
    比如:公众号表Mp,能即时显示每个公众号,对应有哪些Subscriber(订阅者)、有哪些Article(文章),有些是通过多对多(M2M)查询得到的。
    超过20条记录,自动分页pagination


    Mp.png
  3. 加上权限保护,只有superuser才能访问后台管理
    当然,也可以定制,让不同权限用户,能查看不同的内容
  4. 提供搜索功能
    比如,在用户表User,我想把某个用户加入黑名单,设想一下,如果有成千上万的用户,不可能一页页找吧?所以加入搜索框:


    User-search.png

1. 对模型model,有CRUD基本功能

我们先看一下Get_started最简例子:

admin.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView

app = Flask(__name__)
admin = Admin(app, name='MyAdmin', template_mode='bootstrap3')

# Flask-SQLAlchemy initialization here
db = SQLAlchemy(app)

# Create models
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(64))
class Role(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64))

admin.add_view(ModelView(User, db.session))
admin.add_view(ModelView(Role, db.session))

app.run(debug=True)

去掉注释、import,才短短12行代码?!
保存为admin.py,运行python admin.py
打开浏览器:http://localhost:5000/admin
是不是立马显示出Bootstrap风格的后台管理页面了?
只不过没有关联真实数据库,所以内容是空的。

Get_start

下面把Flask-Admin整合到我们app应用,并且关联微信公众号RSS的真实数据库:

  • Flask-Admin初始化

/app/_init_.py

# encoding: utf-8
from flask import Flask, abort, redirect, url_for, request
from flask_sqlalchemy import SQLAlchemy
from config import config
from flask_admin.contrib import sqla
from flask_admin import Admin, helpers as admin_helpers

db = SQLAlchemy()

# models引用必须在 db/login_manager之后,不然会循环引用
from .models import User, Role

# Create admin
admin = Admin(name=u'简读Admin')

def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)

    db.init_app(app)
    admin.init_app(app)

    return app
  • 自定义后台管理页面的首页

/app/templates/admin/index.html

{% extends 'admin/master.html' %}
{% block body %}
<div class="container" align="right">
 <h5 align="center">Welcome to 后台管理!</h5>
    <br>
    <p>管理员<a href="/login">登录</a></p>
    <br>
    Back to <a href="/">首页 - 简读RSS</a>
<div>
{% endblock %}
  • 在View视图里,为Admin引入我们应用里的数据模型

/app/main/views.py

from flask import render_template, redirect, url_for, abort, flash, request,\
    current_app, make_response, jsonify
from .. import db, admin
from flask_admin import Admin, BaseView, expose
from flask_admin.contrib.sqla import ModelView
from ..models import User, Mp, Article, Role, Subscription

# 后台管理页面的首页
class MyView(BaseView):
    @expose('/')
    def index(self):
        return self.render('admin/index.html')

admin.add_view(ModelView(Role, db.session))
admin.add_view(ModelView(User, db.session))
admin.add_view(ModelView(Mp, db.session))
admin.add_view(ModelView(Subscription, db.session))
admin.add_view(ModelView(Article, db.session))

运行应用:python manage.py runserver
有数据显示啦!

ModelView-default

等等!为什么Subscriber、Mp显示的是<models.XXX object>??

看一下 /app/models.py里Subscription定义,原来这两个字段,是外键ForeignKey,查到当然是对象了

# 订阅公众号和User是多对多关系
class Subscription(db.Model):
    __tablename__ = 'subscriptions'
    id = db.Column(db.Integer(), primary_key=True)
    subscriber_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    mp_id = db.Column(db.Integer, db.ForeignKey('mps.id'))
                         #   primary_key=True)

解决:
Python对象有个_repr方法,可以方便地改变打印对象时的输出
我们来改一下User、Mp对象的_repr
方法:

class User(db.Model):
    ...
    def __repr__(self):
        return '<User-%d %r>' % (self.id, self.email)

class Mp(db.Model):
    ...
    def __repr__(self):
        return '<Mp-%d %s>' % (self.id, self.mpName)

这时,再访问一下http://localhost:5000/admin/subscription/, 哈哈,清楚地显示出对象了吧?不再是一长串内存地址了

Class_repr

现在,给其它的模型Role, Subscription, Article都加上__repr__()吧!
加上之后,另一个好处是:使用Create创建/Modify修改数据库记录时,一目了然,也不用面对一长串内存地址了:

create_user.png

再看一下User页面:
我的天,密码都显示出来啦!幸好是加密过的!


User-default

一长串看着有些碍眼,如何修改?而且万一有些字段我们想保密,怎么办呢?

解决:
Flask-Admin提供全方位的定制服务,除了完善的默认显示,另外你想怎么显示就怎么显示。

/app/main/views.py

创建一个自定义ModelView,User.password字段,比如,我只想显示后6位。然后添加给User:

class MyModelViewUser(ModelView):
    # 字段(列)格式化
    # `view` is current administrative view
    # `context` is instance of jinja2.runtime.Context
    # `model` is model instance
    # `name` is property name
    column_formatters = dict(
        password=lambda v, c, m, p: '**'+m.password[-6:],
        )

admin.add_view(MyModelViewUser(User, db.session))

再试试看,这下顺眼多了吧。

User-password customize

2. 显示数据库内容,包括关系表Relation对应的内容

上一节,最后一张图,缺省情况下不显示Roles, Mps,因为它们是关系表Relationship。id,默认也是不显示的,因为是主键primary_key:

# /app/models.py
class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    。。。
    roles = db.relationship('Role', secondary=roles_users,
                            backref=db.backref('users'), lazy='dynamic')
    mps = db.relationship('Subscription',
                               foreign_keys=[Subscription.subscriber_id],
                               backref=db.backref('subscriber', lazy='joined'),
                               lazy='dynamic',      # select dynamic subquery
                               cascade='all, delete-orphan')

那如何显示呢?
继续在/app/main/views.py里配置:

class MyModelViewUser(ModelView):
    column_display_pk = True # optional, but I like to see the IDs in the list
    column_display_all_relations = True

咦,报错了:

sqlalchemy.exc.InvalidRequestError
InvalidRequestError: 'User.roles' does not support object population - eager loading cannot be applied.

原来,User模型里,roles relationship没有定义外键,而且是用了lazy='dynamic'查询方式,每次动态查询,但不会立即返回数据。

解决:
把User--roles字段,lazy='dynamic'注释掉!

这下,没有报错了,主键id、关系Relationship都显示了。但Mps怎么是SQL语句啊??

Relationship

如果把User--mps字段,lazy='dynamic'注释掉,会怎么样呢?
答案是:Mps会显示内容了!但,因为mps字段是多对多关系的外键,这样Flask后台查询语句会报错,比如Mp.to_json()!

解决:
User模型保留lazy='dynamic',添加subscribed_mps_str属性方法,供ModelView里自定义显示Mps字段使用

# /app/models.py
class User(UserMixin, db.Model):
    ...
    @property
    def subscribed_mps_str(self):
        mplist = [] 
        i = 1
        # SQLAlchemy 过滤器和联结
        mps = Mp.query.join(Subscription, Subscription.mp_id == Mp.id)\
            .filter(Subscription.subscriber_id == self.id)
        for mp in mps:
                mplist.append('<Mp-%d %s_%s>' % (mp.id, mp.weixinhao, mp.mpName) )
                i+=1
        return mplist

Admin ModelView里自定义显示Mps字段,使用subscribed_mps_str方法。
顺便重构一下,把公用的ModelView定义到MyModelViewBase

# /app/main/views.py
class MyModelViewBase(ModelView):
   column_display_pk = True # optional, but I like to see the IDs in the list
   column_display_all_relations = True

class MyModelViewUser(MyModelViewBase):
    column_formatters = dict(
        password=lambda v, c, m, p: '**'+m.password[-6:],
        mps=lambda v, c, m, p: (m.subscribed_mps_str),  
        )

admin.add_view(MyModelViewUser(User, db.session))

OK了,按我们的期望显示了:


User.png

3. 加上权限保护,只有superuser才能访问后台管理

这就用到我们上一篇的Flask-Security里,session管理的功能了
在自定义ModelView里,添加权限检查就行:

# /app/main/views.py
class MyModelViewBase(ModelView):
    def is_accessible(self):
        if not current_user.is_active or not current_user.is_authenticated:
            return False
        if current_user.has_role('superuser'):
            return True
        return False
        
    def _handle_view(self, name, **kwargs):
        """
        Override builtin _handle_view in order to redirect users when a view is not accessible.
        """
        if not self.is_accessible():
            if current_user.is_authenticated:
                # permission denied
                abort(403)
            else:
                # login
                return redirect(url_for('security.login', next=request.url))

试一下,是不是没权限访问啦?乖乖地用admin登录吧~
记得先创建superuser用户,用户名admin,密码自己定:

python manage.py initrole

4. 提供搜索功能

其实很简单,ModelView里添加column_searchable_list就行。
注意,不同的模型,search字段要分开来。如果放在一起,会查询错误,因为不同模型(比如User, Article),不一定定义了Relationship关系。

# /app/main/views.py
class MyModelViewUser(MyModelViewBase):
    column_formatters = dict(
        password=lambda v, c, m, p: '**'+m.password[-6:],
        mps=lambda v, c, m, p: (m.subscribed_mps_str),  
        )
    column_searchable_list = (User.email, )
    
class MyModelViewMp(MyModelViewBase):
    column_formatters = dict(
        subscribers=lambda v, c, m, p: (m.subscribers_str), # '\n\p'.join
        articles=lambda v, c, m, p: (m.articles_str),
        )
    column_searchable_list = (Mp.weixinhao, Mp.mpName, )

admin.add_view(MyModelViewBase(Role, db.session))
admin.add_view(MyModelViewUser(User, db.session))
admin.add_view(MyModelViewMp(Mp, db.session))
admin.add_view(MyModelViewBase(Subscription, db.session))
admin.add_view(MyModelViewBase(Article, db.session))

Demo:http://vue2.heroku.com
源码:https://code.csdn.net/Kevin_QQ/vue-tutorial/tree/master

敬请关注第7篇!
Vue 2.0 起步(7) 大结局:公众号文章抓取 - 微信公众号RSS
Vue 2.0 起步(6) 后台管理Flask-Admin更新

http://www.jianshu.com/p/ab778fde3b99

推荐阅读更多精彩内容