Flask学习笔记_BBS论坛搭建(三)


这一章主要是项目实战中一些笔记记录,flask 系列的代码笔记都放在了 仓库

1.cms管理

1.1.项目模块划分,目录结构搭建。

1.2.每个模块注册蓝图,并绑定。

#1.views.py注册蓝图
from flask import Blueprint
bp=Blueprint('cms',__name__,url_prefix='/cms')
@bp.route('/')
def index():
    return "cms index"
#2.__init__.py导入蓝图
from .views import bp
#3.app.py绑定蓝图
from apps.cms import bp as cms_bp
app.register_blueprint(cms_bp)

1.3.数据库配置,连接,迁移控制。这里本来用了flask-script,但migrate的新版不支持他了,所以就不用script了。

#1.config.py文件中配置数据库信息
DEBUG=True
DB_USERNAME="root"
DB_PASSWORD="xxx"
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_NAME="bbs_cms"
DB_URI="mysql+pymysql://%s:%s@%s:%s/%s?charste='utf8" % (DB_USERNAME,DB_PASSWORD,DB_HOST,DB_PORT,DB_NAME)
SQLALCHEMY_DATABASE_URI=DB_URI
SQLALCHEMY_TRACK_MODIFIER=False
#2.exts.py文件中导入sqlalchemy的实例化对象db
from flask_sqlalchemy import SQLAlchemy
db=SQLAlchemy()
#3.在app.py文件中用flask-migrate管理数据库
from flask_migrate import Migrate
from exts import db
def create_app():
    app=Flask(__name__)
    app.config.from_object(config)
    db.init_app(app)
    app.register_blueprint(cms_bp)
    app.register_blueprint(front_bp)
    app.register_blueprint(common_bp)
    migrate=Migrate(app,db)
    return app
if __name__ =="__main__":
    app=create_app()
    app.run()

1.4.cms用户模型建立,并把模型映射到数据中

#1.models.py文件中建立模型
from exts import db
from datetime import datetime
class CMSUser(db.Model):
    __tablename__ = 'cms_user'
    id = db.Column(db.Integer,primary_key=True,autoincrement=True)
    username=db.Column(db.String(50),nullable=False)
    password=db.Column(db.String(50),nullable=False)
    email=db.Column(db.String(50),nullable=False,unique=True)
    join_time=db.Column(db.DateTime,default=datetime.now())
#2.模型导入到app.py文件中
from apps.cms import models
#3.在终端中进行映射三部曲
flask db init
flask db migrate
flask db upgrade

1.5.用命令行参数形式添加cms人员。

#1.在commands.py文件中定义命令行创建数据的函数
from apps.cms import models as cms_models
from exts import db
import click
CMSUser=cms_models.CMSUser
@click.option("-e","--email",required=True)
@click.option("-u","--username",required=True)
@click.option("-p","--password",required=True)
def create_cms_user(email,username,password):
    user=CMSUser(username=username,email=email,password=password)
    db.session.add(user)
    db.session.commit()
    print('cms用户添加成功')
#2.在app.py文件中导入并在create-app时,加入命令行函数
import commands
def create_app():
...
app.cli.command("add_cms_user")(commands.create_cms_user)
#3.命令行里面用参数添加数据
flask add_cms_user -e 18811752638@163.com -u zoe -p 111111

1.6.对数据库的密码保存经过加密

#在models,py文件里面对CMSUser模型重新定义,数据库内部用_password,但外部操作都用password,并进行hash加密解密
from exts import db
from datetime import datetime
from werkzeug.security import generate_password_hash,check_password_hash
class CMSUser(db.Model):
    __tablename__ = 'cms_user'
    id = db.Column(db.Integer,primary_key=True,autoincrement=True)
    username=db.Column(db.String(50),nullable=False)
    _password=db.Column(db.String(200),nullable=False)
    email=db.Column(db.String(50),nullable=False,unique=True)
    join_time=db.Column(db.DateTime,default=datetime.now())
    def __init__(self,username,password,email):
        self.username=username
        self.password=password
        self.email=email
    @property#讲一个方法变成属性,可属性方式访问,即user.password,返回的是self._password。
    def password(self):
        return self._password
    @password.setter#定义了password的set方法,即user.password('abc'),将值传入
    def password(self,raw_password):
        self._password=generate_password_hash(raw_password)
    def check_password(self,raw_password):
        result=check_password_hash(self.password,raw_password)#这里的self.password调用了上面的函数,return self._password
        return result

1.7.cms前端登录页面设计

get请求渲染

#1.在view里面创建视图类,并注册到蓝图
class LoginView(views.MethodView):
	def get(self,message=None):
       return render_template('cms/cms_login.html',message=message)
    def post(self):
        pass
bp.add_url_rule('/login/', view_func=LoginView.as_view('login'))
#2.cms_login.html,在bootstrap中文网,中文文档,入门,实例精选中找到符合自己的右键将html页面复制进去,然后修改:
<link href="http://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">#首先要引入bootstrap
<link rel="icon" href="{{url_for('static',filename='common/pic/bbs.ico')}}">#修改icon图片的地址
<link href="{{url_for('static',filename='cms/css/signin.css')}}" rel="stylesheet">#css样式的地址,和一些中文显示
{%if message %}#报错原因通过message拿到并渲染,当为None时,不渲染
    <p class="alert alert-danger" style="text-align:center" role="alert">{{message}}</p>
{%endif%}

post表单提交

#1.在cms_login.html中修改提交方式为post,并对输入的内容添加name属性,后台会从name获取值
<form class="form-signin" method="post">
<input type="email" id="inputEmail" class="form-control" placeholder="邮箱" name="email" required autofocus> 		
<input type="password" id="inputPassword" class="form-control" placeholder="密码" name="password" required>
<input type="checkbox" value="1" name="remember"> 记住我
#2.表单提交之前需要在forms.py中对表单进行前端验证
from wtforms import Form,StringField,IntegerField
from wtforms.validators import Email,InputRequired,Length
class LoginForm(Form):
    email=StringField(validators=[Email(message="请输入正确的邮箱格式"),InputRequired(message="请输入邮箱")])
    password=StringField(validators=[InputRequired(message="请输入密码"),Length(6,20,message="密码长度不能小于6,大于20")])
    remember=IntegerField()
#3.在views.py中导入表单验证form,并与后端数据库的数据进行比较处理,所以也要导入数据库模型文件model,并保留用户id到session中,
from .forms import LoginForm
from .models import CMSUser
import config
 def post(self):
        form=LoginForm(request.form)
        if form.validate():
            email=form.email.data
            password=form.password.data
            remeber=form.password.data
            user=CMSUser.query.filter_by(email=email).first()#通过email找到用户
            if user and user.check_password(password):
                session[config.CMS_USER_ID]=user.id
                if remeber :
                    session.permanent=True#保留31天
                return redirect(url_for('cms.index'))
            else:
                return self.get(message="用户或密码错误")

        else:
            message=form.errors.popitem()[1][0]
            return self.get(message=message)
#4.使用session一定要有key,所以在config文件中加入key,并且session中保留的user.id赋值给CMS_USER_ID
import os
SECRET_KEY=os.urandom(24)
CMS_USER_ID="NJNJNKJ"#任意给一个值

1.8.cms的首页登录限制,没有登录不可进入到cms的首页

#1.在decorates.py文件中,创建登录限制的装饰器,如果session中没有user_id就重定向到登录页面
from flask import session,redirect,url_for
from functools import wraps
import config
def login_required(func):
    @wraps(func)
    def inner(*args, **kwargs):
        if config.CMS_USER_ID in session:
            return func(*args, **kwargs)
        else:
            return redirect(url_for('cms.login'))
    return inner
#2.在view.py文件中导入自定义的装饰器,然后放到给函数,这里路由装饰器一定要在其他装饰器前面
from .decorates import login_required
@bp.route('/')
@login_required
def index():
    return "cms index"

1.9.cms的首页

#1.在17素材网等网站找前端网页模板,然后修改html里面js和css的地址,用url_for的方式导入,然后在view中render_template
<script src="{{url_for('static',filename='cms/js/cms_base.js')}}"></script>
#2.在_macros.html中将{{url_for('static',filename=“xx”}}抽象为宏
{% macro static(filename) -%}
    {{url_for("static", filename=filename)}}
{%- wendmacro %}
#3.在cms_index.html中导入宏,并引入css和js文件
{%from "common/_macros.html" import static%}
<link rel="stylesheet" href="{{static('cms/css/cms_base.css')}}">

1.10.cms首页的user通过session中的user_id拿到user,绑到g上,然后在模板渲染时给出user_name;注销按钮取消session中user_id.

#1.在hooks.py文件中将现在登录的用户绑到g上
from .views import bp 
from flask import session,g
import config
from .models import CMSUser
#钩子函数,befor_request:在每次请求之前执行
@bp.before_request
def before_request():
    if config.CMS_USER_ID in session:
        user_id =session.get(config.CMS_USER_ID)
        user=CMSUser.query.get(user_id)
        if user:#如果用户存在,就绑到g.cms_user
            g.cms_user=user
#2.此时,这个钩子函数与app文件没有关联,因此不会执行,所以需要在__init__.py文件中导入
import apps.cms.hooks
#3.在cms_index.html文件中拿到g里面的user,并显示他的name
<li><a href="{{url_for('cms.profile')}}">{{g.cms_user.username}}</a></li>
#4.在view.py文件中写注销函数
@bp.route('/logout/')
@login_required#先验证登录,登陆了才有注销功能,删掉session内容,再重定向到登录页面
def logout():
    del session[config.CMS_USER_ID]
    return redirect(url_for('cms.login'))
#5.在cms_index.html文件中对注销按钮的跳转函数写一下。
<li><a href="{{url_for('cms.logout')}}">注销</a></li>

1.11.cms模板抽离和个人信息页面

#1.将页面的共性内容抽离成父模板cms_base.html,子模版内容用{% block xxx%}{% endblock %}填充,子模版通过{% extends 'cms/cms_base.html' %}继承父模板,所以cms_profile.html可以如下
{% extends 'cms/cms_base.html' %}
{% block title %}
    个人信息
{% endblock %}
{% block page_title %}
    {{ self.title() }}
{% endblock %}
{% block main_content %}
    {% set user=g.cms_user %}
    <table class="table table-bordered">
        <tbody>
            <tr>
                <td>用户名:</td>
                <td>{{user.username}}</td>
            </tr>
            <tr>
                <td>邮箱:</td>
                <td>{{user.email}}</td>
            </tr>
            <tr>
                <td>角色:</td>
                <td>暂未实现</td>
            </tr>
            <tr>
                <td>权限:</td>
                <td>暂未实现</td>
            </tr>
            <tr>
                <td>加入时间:</td>
                <td>{{user.join_time}}</td>
            </tr>
        </tbody>
    </table>
{% endblock %}
#2.在views.py文件中写路由函数并渲染html
@bp.route('profile')
@login_required
def profile():
    return render_template('cms/cms_profile.html')
#3.在cms_base.html中给个人信息href
<li><a href="{{url_for('cms.profile')}}">个人信息</a></li>

1.12.cms登陆页面CSRF保护

#1.在app.py文件中给app绑定csrf保护
from flask_wtf import CSRFProtect
def create_app():
    ...
    CSRFProtect(app)
#2.在cms_login.html和cms_base.html文件的表单中,加入隐藏的csrf——token,函数自动生成token
<input type="hidden" name="csrf_token" value="{{csrf_token()}}">
在cms_base.html的head添加
<meta  name="csrf-token" content="{{csrf_token()}}">

1.13.修改密码功能

get

#1.写get请求时渲染的cms_resetpwd.html
{% extends 'cms/cms_base.html' %}
{% from 'common/_macros.html' import static %}
{% block title %}
修改密码
{% endblock %}

{% block page_title %}
    {{self.title()}}
{% endblock %}

{% block  head %}
    <style>
        .form-container{
            width:300px;
        }
    </style>
{% endblock %}

{% block main_content %}
<form action="" method="post" class="form-min">
    <div class="form-container">
        <div class="form-group">
            <div class="input-group">
                <span class="input-group-addon">原密码</span>
                <input type="password" name="oldpwd" placeholder="请输入原始密码" class="form-control">
            </div>
        </div>
        <div class="form-group">
            <div class="input-group">
                <span class="input-group-addon">新密码</span>
                <input type="password" name="newpwd" placeholder="请输入新密码" class="form-control">
            </div>
        </div>
        <div class="form-group">
            <div class="input-group">
                <span class="input-group-addon">确认新密码</span>
                <input type="password" name="newpwd_repeat" placeholder="请确认新密码" class="form-control">
            </div>
        </div>
        <div class="form-group">
            <button class="btn btn-primary" id="submit">
                立即保存
            </button>
        </div>
    </div>
</form>
{% endblock %}
#2.写view的get函数并注册到url上
class ResetPwdView(views.MethodView):
    decorators = [login_required]
    def get(self):
        return render_template('cms/cms_resetpwd.html')
    def post(self):
        pass
bp.add_url_rule('/resetpwd', view_func=ResetPwdView.as_view('resetpwd'))
#3.修改cms_base.html文件里按修改密码按钮时的跳转
<li><a href="{{url_for('cms.resetpwd')}}">修改密码</a></li>

post

#1.这里用到了ajax技术进行网页异步数据更新,因此在cms_resetpwd.html中倒入resetpwd.js
<script src="{{static('cms/js/resetpwd.js')}}"></script>
#1.前端js:resetpwd.js
$(function(){
    $('#submit').click(function(event){
        event.preventDefault();//阻止按钮默认的提交表单的事件
        var oldpwdInput = $('input[name=oldpwd]');
        var newpwdInput = $('input[name=newpwd]');
        var newpwdRepeatInput = $('input[name=newpwd_repeat]');
        var oldpwd = oldpwdInput.val();
        var newpwd = newpwdInput.val();
        var newpwd_repeat = newpwdRepeatInput.val();
        zlajax.post({
            'url': '/cms/resetpwd/',
             'data': {
                 'oldpwd': oldpwd,
                 'newpwd': newpwd,
                 'newpwd_repeat': newpwd_repeat
             },
             'success': function (data) {
                 if(data['code'] == 200){
                     oldpwdInput.val('');
                     newpwdInput.val('');
                     newpwdRepeatInput.val('');
                     xtalert.alertSuccessToast('恭喜您!密码修改成功!');
                 }else{
                     xtalert.alertInfoToast(message);
                 }
             },
             'fail': function (error) {
                 // console.log(error);
                 xtalert.alertNetworkError();
             }
         });
    })
})
#2.在cms_base.html的head引入这个上面所引用的zlajax.js
<script src="{{static('common/js/zlajax.js')}}"></script>
#2.表单验证
#3.views.py的post部分

1.14.修改邮箱功能

get

#1.cms_resetemail.html
{% extends "cms/cms_base.html" %}
{% block title -%}
    修改邮箱
{%- endblock %}
{% block head %}
{% endblock %}
{% block page_title -%}
    {{self.title()}}
{%- endblock %}
{% block main_content %}
    <form action="" method="post">
        <div class="form-container">
            <div class="form-group">
                <div class="input-group">
                    <input type="email" class="form-control" name="email" placeholder="新邮箱">
                    <span class="input-group-addon" id="captcha-btn" style="cursor: pointer;">获取验证码</span>
                </div>
            </div>
            <div class="form-group">
                <input type="text" class="form-control" name="captcha" placeholder="邮箱验证码">
            </div>
            <div class="form-group">
                <button class="btn btn-primary" id="submit">立即修改</button>
            </div>
        </div>
    </form>
{% endblock %}
#2.views.py文件写视图函数,并映射url
class ResetEmailView(views.MethodView):
    decorators=[login_required]
    def get(self):
        return render_template('cms/cms_resetemail.html')
    def post(self):
        pass
bp.add_url_rule('/resetemail/',view_func=ResetEmailView.as_view('resetemail'))
#3.在cms_base.html中修改修改邮箱的跳转href
<li><a href="{{url_for('cms.resetemail')}}">修改邮箱</a></li>

使用flask-mail的邮箱验证码功能和ajax获取验证码

pip install Flask-Mail
#1.exts.py中初始化mail
from flask_mail import Mail
mail=Mail()
#2.在app.py中与app进行绑定
from exts import mail
mail.init_app(app)
#3.config.py文件中对mail进行配置
MAIL_SERVER="smtp.qq.com"#发送验证码的邮箱服务器,这里是自己公司的邮箱服务器
MAIL_PORT='587'#587是tls协议,465是ssl协议
MAIL_USE_TLS=True
#MAIL_USE_SSL
MAIL_USERNAME="1163xx@qq.com"
MAIL_PASSWORD="wxxx"
MAIL_DEFAULT_SENDER="1163xx@qq.com"
#4.在view.pt写发送验证码的路由函数,这里用get的方式拿到email,然后生成验证码并发送
from exts import mail
from flask_mail import Message
import string,random
@bp.route("/email_captcha/")
def email_captcha():
    email=request.args.get('email')#拿到邮箱信息
    if not email:
        return restful.paramserror("请填写邮箱")
    source=list(string.ascii_letters)
    source.extend(map(lambda x:str(x),range(0,10)))
    captcha="".join(random.sample(source,6))#生成了验证码
    message=Message('cms邮箱验证码',recipients=[email],body="您的验证码是:%s"%captcha)
    try:
        mail.send(message)
    except:
        return restful.servererror()
    return restful.success("")
#5.点击获取验证码按钮,那么就通过ajax方式调用上面的路由函数,resetemail.js
$(function(){
    $("#captcha-btn").click(function(event){
        event.preventDefault();
        var email=$("input[name='email']").val();
        if (!email){
            zlalert.alertInforToast('请输入邮箱');
            return ;
        }
        zlajax.get({
            'url':'/cms/email_captcha/',
            'data':{'email':email},
            'success':function(data){
                if(data['code']==200){
                    zlalert.alertSuccessToast('邮件发送成功,请注意查收!');
                }else{
                    zlalert.alertInfo(data['message']);
                }
            },
            'fail':function(error){
                zlalert.alertNetworkError();
            }
        });
    });
});
#6.将这个js文件引入到cms_resetemail.html文件中
{% from "common/_macros.html" import static %}{% block head %}
    <script src="{{static('cms/js/resetemail.js')}}"></script>
{% endblock %}

点击立即修改,开始提交表单,与数据库进行交互

#1.因为要把邮箱和验证码绑定到一起并存到memcache中,因此在zlcache.py文件中
import memcache
cache=memcache.Client(['127.0.0.1:11211'],debug=True)
def set(key,value,timeout=60):
    return cache.set(key,value,timeout)
def get(key):
    return cache.get(key)
def delete(key):
    return cache.delete(key)
#2.在views.py文件的email_captcha视图中添加绑定
from utils import zlcache
zlcache.set(email,captcha)
#3.在forms.py中进行表单验证
from utils import zlcache
from wtforms import ValidationError
class ResetEmailForm(BaseForm):
    email=StringField(validators=[Email(message="请输入正确的邮箱格式")])
    captcha=StringField(validators=[Length(min=6,max=6,message="请输入6位验证码")])
    def validate_captcha(self,field):
        captcha=field.data
        email=self.email.data
        captcha_cache=zlcache.get(email)
        if not captcha_cache or captcha.lower()!=captcha_cache.lower():
            raise ValidationError('邮箱验证码错误')
    def validate_email(self,field):
        email=field.data
        user=g.cms_user
        if user.email==email:
            raise ValidationError("新邮箱不能是旧邮箱")
#4.在view.py文件中补充post
from .forms import ResetEmailForm
def post(self):
        form=ResetEmailForm(request.form)
        if form.validate():
            email=form.email.data
            g.cms_user.email=email
            db.session.commit()
            return restful.success()
        else:
            return restful.paramserror(form.get_error())
#5.在resetemail.js文件中对立即修改按钮的点击时间获取并处理
$(function(){
    $('#submit').click(function(event){
        event.preventDefault();
        var emailE=$("input[name='email']");
        var captchaE=$("input[name='captcha']");
        var email=emailE.val();
        var captcha=captchaE.val();
        zlajax.post({
            'url':'/cms/resetemail/',
            'data':{'email':email,'captcha':captcha},
            'success':function(data){
                if (data['code']==200){
                	emailE.val("");
                    captchaE.val("");
                    zlalert.alertSuccessToast("恭喜,邮箱修改成功");
                }else{
                    zlalert.alertInfo(data['message']);
                }
            },
            'fail':function(error){
                zlalert.alertNetworkError();
            }
        });
    });
});

1.15.cms权限和角色管理

角色和用户是多对多的关系

#1.在models.py文件中定义角色表,角色表和用户表的中间表和关系
class CMSPermission(object):#权限类
    ALL_PERMISSION=0b11111111#255的二进制方式
    VISITOR=0b00000001#访问权限
    POSTER=0b00000010#管理帖子权限
    COMMENTER=0b00000100#管理评论权限
    BOARDER=0b00001000#管理板块权限
    FRONTUSER=0b00010000#管理前台用户权限
    CMSUSER=0b00100000#管理后台用户权限
    ADMINER=0b01000000#管理后台管理员的权限
#角色和用户的中间表
cms_role_user=db.Table('cms_role_user',db.Column('cms_role_id',db.Integer,db.ForeignKey('cms_role.id'),primary_key=True),
                       db.Column('cms_user_id',db.Integer,db.ForeignKey('cms_user.id'),primary_key=True))

class CMSRole(db.Model):#角色类
    __tablename__='cms_role'
    id=db.Column(db.Integer,primary_key=True,autoincrement=True)
    name=db.Column(db.String(50),nullable=False)
    desc=db.Column(db.String(200),nullable=True)
    creat_time=db.Column(db.DateTime,default=datetime.now)
    permissions=db.Column(db.Integer,default=CMSPermission.VISITOR)
    users=db.relationship('CMSUser',secondary=cms_role_user,backref='roles')
#2.在终端把数据库映射
flask db migrate
flask db upgrade
#3.在commands.py文件中定义角色的生成函数
CMSRole=cms_models.CMSRole
CMSPermission=cms_models.CMSPermission
def create_role():
    visitor=CMSRole(name='访问者',desc='只能查看相关数据,不能修改。')
    visitor.permissions=CMSPermission.VISITOR
    operator=CMSRole(name='运营',desc='管理帖子,评论和前台用户。')
    operator.permissions=CMSPermission.VISITOR|CMSPermission.POSTER|CMSPermission.COMMENTER|CMSPermission.FRONTUSER
    admin=CMSRole(name='管理员',desc='拥有本系统所有权限。')
    admin.permissions=CMSPermission.VISITOR|CMSPermission.POSTER|CMSPermission.CMSUSER|CMSPermission.COMMENTER|CMSPermission.FRONTUSER|CMSPermission.BOARDER
    developer=CMSRole(name='开发者',desc='开发人员专用角色。')
    developer.permissions=CMSPermission.ALL_PERMISSION
    db.session.add_all([visitor,operator,admin,developer])
    db.session.commit()
#4.在app.py文件中对app添加这个command
app.cli.command("create_role")(commands.create_role)
#5.在cmd命令中调用命令,生成角色数据
flask create_role
#6.在commands.py文件中根据用户email,给用户添加角色,并用cmd方式添加
@click.option("-e","--email",required=True)
@click.option("-r","--role",required=True)
def add_user_to_role(email,role):
    user=CMSUser.query.filter_by(email=email).first()
    if user:
        role=CMSRole.query.filter_by(name=role).first()
        if role:
            role.users.append(user)
            db.session.commit()
            print("用户添加到角色成功")
        else:
            print("没有%s这个角色"%role)
    else:
        print("%s邮箱的用户不存在"%email)
#7.并将命令添加到app上
app.cli.command("add_user_to_role")(commands.add_user_to_role)
测试:flask add_user_to_role -e 18811752638@163.com -r 访问者
#8.在models.py文件的CMSUser类里面添加用户的权限属性,和判断用户是否有某权限的方法,并判断是否是开发者
	@property
    def permissions(self):
        if not self.roles:
            return None
        all_permissions=0
        for role in self.roles:
            all_permissions|=role.permissions
        return all_permissions
    def has_permission(self,permission):
                if not self.roles:
            return False
        all_permissions = 0
        for role in self.roles:
            all_permissions |= role.permissions

        if isinstance(permission, str):
            permission = getattr(CMSPermission, permission, 0)

        return all_permissions & permission == permission
    @property
    def is_developer(self):
        return self.has_permission(CMSPermission.ALL_PERMISSION)
#9.在commands.py中利用上述属性和方法,检查获取用户的角色权限
def test_permission():
    user=CMSUser.query.first()
    if user.has_permission(CMSPermission.VISITOR):
        print("这个用户有访问者权限")
    else:
        print("这个用户没有访问者权限")
#10.添加到app
app.cli.command("test_permission")(commands.test_permission)
测试:flask test_permission

客户端权限验证功能

#1.修改cms_profile.html
 			<tr>
                <td>角色:</td>
                <td>
                    {% for role in user.roles %}
                        {{role.name}}
                        {% if not loop.last %},{% endif %}
                    {% endfor %}
                </td>
            </tr>
            <tr>
                <td>权限:</td>
                <td>
                    {% for role in user.roles %}
                        {{role.desc}}
                        {% if not loop.last %}/{% endif %}
                    {% endfor %}
                </td>
#2.在hooks.py文件中将cms的permission添加到上下文处理器当中,以后就可以次bp蓝图的在html等上下文中直接使用
from .models import CMSPermission
@bp.context_processor
def cms_context_processor():
    return {"CMSPermission":CMSPermission}
#3.在cms_base.html中修改界面根据权限显示
  				{% set cms_user=g.cms_user %}
                {% if cms_user.has_permission(CMSPermission.POSTER) %}
                  <li class="nav-group post-manage"><a href="#">帖子管理</a></li>
                {% endif %}
                {% if cms_user.has_permission(CMSPermission.COMMENTER) %}
                <li class="comments-manage"><a href="#">评论管理</a></li>
                {% endif %}
                {% if cms_user.has_permission(CMSPermission.BOARDER) %}
                <li class="board-manage"><a href="#">板块管理</a></li>
                {% endif %}
                {% if cms_user.has_permission(CMSPermission.FRONTUSER) %}
                <li class="board-manage"><a href="#">前台管理</a></li>
                {% endif %}
                {% if cms_user.has_permission(CMSPermission.CMSUSER) %}
                <li class="nav-group user-manage"><a href="#">CMS用户管理</a></li>
                {% endif %}
                {% if cms_user.is_developer %}
                <li class="cmsrole-manage"><a href="#">CMS组管理</a></li>
                {% endif %}

服务端权限验证功能

#1.创建html,在view里面写路由,在base里面写每一个按钮对应的href
#2.在decorates.pyh中写权限装饰器
def permission_required(permission):
    def outter(func):
        @wraps(func)
        def inner(*args,**kwargs):
            user=g.cms_user
            if user.has_permission(permission):
                return func(*args,**kwargs)
            else:
                return redirect(url_for('cms.index'))
        return inner
    return outter
#3.在views里面加上装饰器
from .models import CMSPermission
from .decorates import permission_required
@bp.route("/posts/")
@login_required
@permission_required(CMSPermission.POSTER)
def posts():
    return render_template("cms/cms_posts.html")

1.16.cms轮播图管理

前端

#1.写csm_banners.html
{% extends "cms/cms_base.html"%}
{% block  title%}轮播图管理{% endblock %}
{% block head %}
<script src="{{url_for('static',filename='cms/js/banners.js')}}"></script>
<style>
    .top-box button{
        float:right;
    }
</style>
{% endblock %}
{% block page_title %}{{self.title()}}{% endblock %}
{% block main_content %}
<div class="top-box">
    <button class="btn btn-warning" class="btn btn-primary btn-lg" data-toggle="modal" data-target="#banner-dialog">添加轮播图</button>
</div>
<table class="table table-boardered">
    <thead>
        <tr>
            <th>名称</th>
            <th>图片链接</th>
            <th>跳转链接</th>
            <th>优先级</th>
            <th>创建时间</th>
            <th>操作</th>
        </tr>
    </thead>
    <tbody>
      {% for banner in banners %}
        <tr>
          <td>{{banner.name}}</td>
          <td><a href="{{banner.image_url}}" target="_blank">{{banner.image_url}}</a></td>
          <td><a href="{{banner.link_url}}" target="_blank">{{banner.link_url}}</a></td>
          <td>{{banner.priority}}</td>
          <td>{{banner.creat_time}}</td>
          <td>
            <button class="btn btn-default btn-xs">编辑</button>
            <button class="bt btn-danger btn-xs">删除</button>
          </td>
        </tr>
        {%endfor%}
    </tbody>
  <!-- Modal -->
  <div class="modal fade" id="banner-dialog" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
          <h4 class="modal-title" id="myModalLabel">轮播图</h4>
        </div>
        <div class="modal-body">
            <form class="form-horizontal">
                <div class="form-group">
                    <label class="col-sm-2 control-label" >名称:</label>
                    <div class="col-sm-10">
                      <input type="text" class="form-control" name="name" placeholder="轮播图名称">
                    </div>
                </div>
                <div class="form-group">
                    <label class="col-sm-2 control-label">图片:</label>
                    <div class="col-sm-7">
                      <input type="text" class="form-control" name="image_url" placeholder="轮播图图片">
                    </div>
                    <button class="btn btn-info col-sm-2">添加图片</button>
                </div>
                <div class="form-group">
                    <label class="col-sm-2 control-label">跳转:</label>
                    <div class="col-sm-10">
                      <input type="text" class="form-control" name="link_url" placeholder="跳转链接">
                    </div>
                </div>
                <div class="form-group">
                    <label class="col-sm-2 control-label">权重:</label>
                    <div class="col-sm-10">
                      <input type="number" class="form-control" name="priority" placeholder="优先级">
                    </div>
                </div>
            </form>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
          <button type="button" class="btn btn-primary" id="save-banner-btn">保存</button>
        </div>
      </div>
    </div>
  </div>
</table>
{%endblock%}
#2.在views.py里面定义视图函数
@bp.route("/banners/")#处理轮播图显示的视图
@login_required
def banners():
    banners=BannerModel.query.all()
    return render_template("cms/cms_banners.html",banners=banners)
#3.在cms_base.html里面加上轮播图管理的跳转连接
<li class="nav-group banner-manage"><a href="{{url_for('cms.banners')}}">轮播图管理</a></li>
#4.在cms_base.js中对点击轮播图的跳转进行红色渲染
 else if(url.indexOf('banners') >= 0) {
        var bannersManageLi = $('.banner-manage');
        bannersManageLi.addClass('unfold').siblings().removeClass('unfold');
    }

后端:在apps文件夹下面新建model.py文件,处理添加轮播图按钮的表单处理

from exts import db
from datetime import datetime
class BannerModel(db.Model):
    __tablename__='banner'
    id=db.Column(db.Integer,primary_key=True,autoincrement=True)
    name=db.Column(db.String(255),nullable=False)
    image_url=db.Column(db.String(255),nullable=False)
    link_url=db.Column(db.String(255),nullable=False)
    priority=db.Column(db.Integer,default=1)
    create_time=db.Column(db.DateTime,default=datetime.now)
#2.在app.py文件里面倒入模型
from apps.models import BannerModel
#3迁移入库
flask db migrate
flask db upgrade
#4.在apps/cms/forms.py文件对表单进行验证
class AddBannerForm(BaseForm):
    name=StringField(validators=[InputRequired(message="请输入轮播图名称")])
    image_url=StringField(validators=[InputRequired(message="请输入轮播图图片")])
    link_url=StringField(validators=[InputRequired(message="请输入轮播图跳转链接")])
    priority=IntegerField(validators=[InputRequired(message="请输入轮播图优先级")])
#5.在apps/cms/views.py文件里面写处理表单的视图函数
from ..models import BannerModel
from .forms import AddBannerForm
@bp.route('/abanner/',methods=['POST'])
@login_required
def abanner():
    form=AddBannerForm(request.form)
    if form.validate():
        name=form.name.data
        image_url=form.image_url.data
        link_url=form.link_url.data
        priority=form.priority.data
        banner=BannerModel(name=name,image_url=image_url,link_url=link_url,priority=priority)
        db.session.add(banner)
        db.session.commit()
        return restful.success()
    else:
        return restful.paramserror(message=form.get_error())

3.点击保存按钮后,用js将前端表单发送到服务器

#1.在static/cms/js/banners.js里面写表单提交服务器的过程
$(function(){
    $("#save-banner-btn").click(function(event){
        event.preventDefault;
        var dialog=$("#banner-dialog");
        var nameInput=$("input[name='name']");
        var imageInput=$("input[name='image_url']");
        var linkInput=$("input[name='link_url']");
        var priorityInput=$("input[name='priority']");
        var name=nameInput.val();
        var image_url=imageInput.val();
        var link_url=linkInput.val();
        var priority=priorityInput.val();
        if (!name||!image_url||!link_url||!priority){
            zlalert.alertInfoToast("请输入完整轮播图数据");
            return ;
        }
        zlajax.post({
            
            'url':'/cms/abanner/',
            'data':{
                'name':name,
                'image_url':image_url,
                'link_url':link_url,
                'priority':priority
            },
            'success':function(data){
                dialog.modal("hide");
                if (data['code']==200){
                    window.location.reload();
                }else{
                    zlalert.alertInfo(data['message']);
                }
            },
            'fail':function(){
                zlalert.alertNetworkError();
            }
        })
    });
});
#2.在cms_banners.html中倒入js文件
<script src="{{url_for('static',filename='cms/js/banners.js')}}"></script>

轮播图的编辑功能

#1.更新表单的验证
class UpdateBannerForm(AddBannerForm):
    banner_id=IntegerField(validators=[InputRequired(message="请输入轮播图的id")])
#2.在views里面写修改表单时视图函数
from .forms import UpdateBannerForm
@bp.route("/ubanner/", methods=["POST"])
@login_required
def ubanner():
    form=UpdateBannerForm(request.form)
    if form.validate():
        banner_id=form.banner_id.data
        name=form.name.data
        image_url=form.image_url.data
        link_url=form.link_url.data
        priority=form.priority.data
        banner=BannerModel.query.get(banner_id)
        if banner:
            banner.name=name
            banner.image_url=image_url
            banner.link_url=link_url
            banner.priority=priority
            db.session.commit()
            return restful.success()
        else:
            return restful.paramserror(message="没有这个轮播图")
    else:
        return restful.paramserror(message=form.get_error())
#1.给编辑功能加一个class:edit-banner-btn
<button class="btn btn-default btn-xs edit-banner-btn">编辑</button>
#2将表单里面的信息绑定到编辑按钮的父标签<td>的父标签<tr>中
 <tr data-name="{{banner.name}}" data-image="{{banner.image_url}}" data-link="{{banner.link_url}}" data-priority="{{banner.priority}}" data-id="{{banner.id}}">
#3.在js里面监听这个class的点击事件,因为是监听类,所以是 $(".edit-banner-btn")
$(function() {
    $(".edit-banner-btn").click(function(event){
    	var self=$(this);
        var dialog = $("#banner-dialog");
        dialog.modal("show");

        var tr=self.parent().parent();
        var name=tr.attr("data-name");
        var image_url=tr.attr("data-image");
        var link_url=tr.attr("data-link");
        var priority=tr.attr("data-priority");

        var nameInput=dialog.find("input[name='name']");
        var imageInput=dialog.find("input[name='image_url']");
        var linkInput=dialog.find("input[name='link_url']");
        var priorityInput=dialog.find("input[name='priority']");
        
        var saveBtn=dialog.find("#save-banner-btn")

        nameInput.val(name);
        imageInput.val(image_url);
        linkInput.val(link_url);
        priorityInput.val(priority);
        saveBtn.attr("data-type",'update');//在save-btn上绑定这个是更新,而不是添加
        saveBtn.attr("data-id",tr.attr('data-id'));//在save-btn上绑定轮播图的id
    });
});
#4.在前面的save-banner-btn判断是更新还是添加
var self=$(this);
var submitType=self.attr("data-type");
var bannerId=self.attr("data-id");
 var url='';
        if (submitType=="update"){
            url='/cms/ubanner/';
        }else{
            url='/cms/abanner/';
        }
        zlajax.post({
            
            'url':url,
            'data':{
                'name':name,
                'image_url':image_url,
                'link_url':link_url,
                'priority':priority,
                'banner_id':bannerId
            },

轮播图的删除功能

#1.在views删除操作的处理视图函数
@bp.route("/dbanner/",methods=['POST'])
@login_required
def dbanner():
    banner_id=request.form.get("banner_id")
    if not banner_id:
        return restful.paramserror(message="请传入轮播图id")
    banner=BannerModel.query.get(banner_id)
    if not banner:
        return restful.paramserror(message="没有这个轮播图")
    db.session.delete(banner)
    db.session.commit()
    return restful.success()
#2.在html给删除按钮一个类标签delete-banner-btn
<button class="bt btn-danger btn-xs delete-banner-btn">删除</button>
#3.在js监听删除按钮
$(function(){
    $(".delete-banner-btn").click(function(event){
        var self=$(this);
        var tr=self.parent().parent();
        var banner_id=tr.attr('data-id');
        zlalert.alertConfirm({
            'msg':'您确定要删除这个轮播图吗?',
            'confirmCallback':function(){
                zlajax.post({
                    'url':'/cms/dbanner/',
                    'data':{'banner_id':banner_id},
                    'success':function(data){
                        if(data['code']==200){
                            window.location.reload();
                        }else{
                            zlalert.alertInfo(data['message']);
                        }
                    }
                })
            }
        });
    });
});

1.17.七牛云存储

#1.后端获取uptoken的接口
pip install qiniu
import qiniu
from flask import jsonify
@app.route('/uptoken/')
    def uptoken():
        access_key='xxx'
        secret_key='xxx'
        q=qiniu.Auth(access_key,secret_key)
        bucket='cssflask'
        token=q.upload_token(bucket)
        return jsonify({'uptoken':token})
#2.在前端添加js的sdk
    <script src="https://cdn.staticfile.org/Plupload/2.1.1/moxie.js"></script>
    <script src="https://cdn.staticfile.org/Plupload/2.1.1/plupload.dev.js"></script>
    <script src="https://cdn.staticfile.org/qiniu-js-sdk/1.0.14-beta/qiniu.js"></script>
#3.添加zlqiniu.js
<script src="{{url_for('static',filename='zlqiniu.js')}}"></script>
#4.初始化七牛
 <script>
        window.onload = function () {
            zlqiniu.setUp({
                'domain': '7xqenu.com1.z0.glb.clouddn.com',
                'browse_btn': 'upload-btn',
                'uptoken_url': '/uptoken/',
                'success': function (up,file,info) {
                    var image_url = file.name;
                    var imageInput = document.getElementById('image-input');
                    imageInput.value = image_url;

                    var img = document.getElementById('img');
                    img.setAttribute('src',image_url);
                }
            });
        }
    </script>
</head>
<body>
    <button id="upload-btn">上传文件</button>
    <input type="text" id="image-input">
    <img src="" alt="" id="img">
</body>

用七牛上传轮播图

#1.在apps/common/views.py里面获取uptoken的接口
from flask import jsonify
@bp.route('/uptoken/')
def uptoken():
    access_key='xxx'
    secret_key='xxx'
    q=qiniu.Auth(access_key,secret_key)
    bucket='cssflask'
    token=q.upload_token(bucket)
    return jsonify({'uptoken':token})
#2.在cms_banners.html里面倒入js的sdk
<script src="https://cdn.staticfile.org/Plupload/2.1.1/moxie.js"></script>
<script src="https://cdn.staticfile.org/Plupload/2.1.1/plupload.dev.js"></script>
<script src="https://cdn.staticfile.org/qiniu-js-sdk/1.0.14-beta/qiniu.js"></script>
#3.倒入zlqiniu.js
<script src="{{url_for('static',filename='common/js/zlqiniu.js')}}"></script>
#4.表单的添加图片按钮加一个id
<button class="btn btn-info col-sm-2 " id='upload-btn'>添加图片</button>
#5.在banners.js中初始化七牛
$(function(){
    zlqiniu.setUp({
        'domain':'http://ry3i8xuci.hb-bkt.clouddn.com/',
        // 'domain':'ry3i8xuci.hb-bkt.clouddn.com',
        'browse_btn': 'upload-btn',
        'uptoken_url': '/c/uptoken/',
        'success': function (up,file,info) {
            var imageInput = $("input[name='image_url']");
            imageInput.val(file.name);
        }
    });
});

1.18.板块管理

#1.写板块的model,并migrate和upgrade映射到数据库
class BoardModel(db.Model):
    __tablename__='board'
    id=db.Column(db.Integer,primary_key=True,autoincrement=True)
    name=db.Column(db.String(20),nullable=False)
    create_time=db.Column(db.DateTime,default=datetime.now)
#2.写板块管理的html页面,将表单信息绑到tr标签上,方便之后的编辑删除操作获取信息
{% extends "cms/cms_base.html" %}
{% block title -%}板块管理{%- endblock %}
{% block head %}
<script src="{{url_for('static',filename='cms/js/boards.js')}}"></script>
{% endblock %}
{% block page_title -%}{{self.title()}}{%- endblock %}
{% block main_content %}
    <div class="top-box">
        <button class="btn btn-warning" style="float: right;" id="add-board-btn">添加新板块</button>
    </div>
    <table class="table table-bordered">
        <thead>
            <tr>
                <th >板块名称</th>
                <th>帖子数量</th>
                <th>创建时间</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            {% for board in boards %}
            <tr data-name="{{board.name}}" data-id="{{board.id}}">
                <td>{{board.name}}</td>
                <td>0</td>
                <td>{{board.create_time}}</td>
                <td>
                    <button class="btn btn-default btn-xs edit-board-btn">编辑</button>
                    <button class="btn btn-danger btn-xs delete-board-btn">删除</button>
                </td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
{% endblock %}
#3.写视图函数渲染html
@bp.route("/boards/")
@login_required
@permission_required(CMSPermission.BOARDER)
def boards():
    board_models=BoardModel.query.all()
    context={'boards':board_models}
    return render_template("cms/cms_boards.html",**context)
#4.写点击“添加新板块”后进入的视图函数,因为提交的表单要验证
class AddBoardForm(BaseForm):
    name=StringField(validators=[InputRequired(message="请输入板块名称")])
from ..models import BoardModel
from .forms import AddBoardForm
@bp.route("/aboard/",methods=['POST'])
@login_required
@permission_required(CMSPermission.BOARDER)
def aboard():
    form=AddBoardForm(request.form)
    if form.validate():
        name=form.name.data
        board=BoardModel(name=name)
        db.session.add(board)
        db.session.commit()
        return restful.success()
    else:
        return restful.paramserror(message=form.get_error())
#5.编辑板块操作
class UpdateBoardForm(AddBoardForm):
    board_id=IntegerField(validators=[InputRequired(message="请输入板块id")])
from .forms import UpdateBoardForm
@bp.route("uboard/",methods=['POST'])
@login_required
@permission_required(CMSPermission.BOARDER)
def uboard():
    form=UpdateBoardForm(request.form)
    if form.validate():
        board_id=form.board_id.data
        name=form.name.data
        board=BoardModel.query.get(board_id)
        if board:
            board.name=name
            db.session.commit()
            return restful.success()
        else:
            return restful.paramserror(message="没有这个板块")
    else:
        return restful.paramserror(message=form.get_error())
#6.删除板块操作
@bp.route("dboard/",methods=['POST'])
@login_required
@permission_required(CMSPermission.BOARDER)
def dboard():
    board_id=request.form.get("board_id")
    if not board_id:
        return restful.paramserror("请传入板块id")
    board=BoardModel.query.get(board_id)
    if not board:
        return restful.paramserror("没有这个板块")
    db.session.delete(board)
    db.session.commit()
    return restful.success()
#7.在boards.js中监听按钮,传递参数到后台的视图函数
$(function(){
    $("#add-board-btn").click(function(event){
        event.preventDefault();
        zlalert.alertOneInput({
            'text':"请输入板块名称",
            'placeholder':"板块名称",
            'confirmCallback':function(inputValue){
                zlajax.post({
                    'url':'/cms/aboard/',
                    'data':{'name':inputValue},
                    'success':function(data){
                        if (data['code']==200){
                            window.location.reload();
                        }else{
                            zlalert.alertInfo(data['message']);
                        }
                    }
                });
            }
        });
    });
});

$(function(){
    $(".edit-board-btn").click(function(){
        var self = $(this);
        var tr=self.parent().parent();
        var name=tr.attr('data-name');
        var board_id=tr.attr('data-id');
        zlalert.alertOneInput({
            'text':'请输入新板块名称',
            'placeholder':name,
            'confirmCallback':function(inputValue){
                zlajax.post({
                    'url':'/cms/uboard/',
                    'data':{'board_id':board_id,'name':inputValue},
                    'success':function(data){
                        if (data['code']==200){
                            window.location.reload();
                        }else{
                            zlalert.alertInfo(data['message']);
                        }
                    }
                });
            }
        });
    })
})
#8.将j导入到html中
<script src="{{url_for('static',filename='cms/js/boards.js')}}"></script>

1.19帖子和精华管理

#1.在apps/models.py里面创建精华帖模型,migrate,upgrade
class HighlightPostModel(db.Model):
    __tablename__="highlight_post"      
    id=db.Column(db.Integer,primary_key=True,autoincrement=True)
    create_time=db.Column(db.DateTime,default=datetime.now)
    post_id=db.Column(db.Integer,db.ForeignKey("post.id"))
    post=db.relationship('PostModel',backref='highlight')
#2.在cms/views.py里面写加精和取消加精的视图函数
@bp.route("/hpost/", methods=["POST"])
@login_required
@permission_required("CMSPermission.POSTER")
def hpost():
    post_id=request.form.get('post_id')
    if not post_id:
        return restful.paramserror(message="请传入帖子id")
    post=PostModel.query.get(post_id)
    if not post:
        return restful.paramserror(message="没有这篇帖子")
    highlight=HighlightPostModel()
    highlight.post=post
    db.session.add(highlight)
    db.session.commit()
    return restful.success()

@bp.route("/uhpost/", methods=["POST"])
@login_required
@permission_required("CMSPermission.POSTER")
def uhpost():
    post_id=request.form.get('post_id')
    if not post_id:
        return restful.paramserror(message="请传入帖子id")
    post=PostModel.query.get(post_id)
    if not post:
        return restful.paramserror(message="没有这篇帖子")
    highlight=HighlightPostModel.query.filter_by(post_id=post_id).first()
    db.session.delete(highlight)
    db.session.commit()
    return restful.success()
#3.在cms的posts视图函数里面加上帖子加去精的按钮
@bp.route("/posts/")
@login_required
@permission_required(CMSPermission.POSTER)
def posts():
    post_list=PostModel.query.all()
    return render_template("cms/cms_posts.html",posts=post_list)
#4.cms_posts.html
{% extends "cms/cms_base.html" %}
{% block title -%}帖子管理{%- endblock %}
{% block head %}
    <script src="{{url_for('static' ,filename='cms/js/cms_posts.js')}}"></script>>
{% endblock %}
{% block page_title -%}{{self.title()}}{%- endblock %}
{% block main_content %}
    <table class="table table-bordered">
        <thead>
            <tr>
                <td>标题</td>
                <td>发布时间</td>
                <td>板块</td>
                <td>作者</td>
                <td>操作</td>
            </tr>
        </thead>
        <tbody>
            {% for post in posts %}
            <tr data-id="{{post.id}}" data-highlight="{{1 if post.highlight else 0}}">
                <td><a  target="blank" href="{{url_for('front.post_detail',post_id=post.id)}}">{{post.title}}</a></td>
                <td>{{post.create_time}}</td>
                <td>{{post.board.name}}</td>
                <td>{{post.author.username}}</td>
                <td>
                    {% if post.highlight %}
                        <button class="btn btn-default btn-xs highlight-btn">取消加精</button>
                    {% else %}
                        <button class="btn btn-default btn-xs highlight-btn">加精</button>
                    {% endif %}
                    <button class="btn btn-danger btn-xs">移除</button>
                </td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
{% endblock %}
#5.cms_posts.js里面写动作的post提交,并引入到html中
$(function(){
    $(".highlight-btn").click(function(){
        var self = $(this);
        var tr=self.parent().parent();
        var post_id=tr.attr("data-id");
        var highlight=parseInt(tr.attr("data-highlight"));
        var url='';
        if (highlight){
            url='/cms/uhpost/';
        }else{
            url='/cms/hpost/';
        }
        zlajax.post({
            'url': url,
            'data': {'post_id': post_id},
            'success': function(data){
                if (data['code']==200){
                    zlalert.alertSuccessToast('操作成功');
                    setTimeout(function(){
                        window.location.reload();
                    },500);
                }else{
                    zlalert.alertInfo(data['message']);
                }
            }
        })
    });
});

2.前台论坛

2.1.前台用户模型创建

#1.pip install shortuuid
#2.在front/models.py里面创建前台用户类
from exts import db
import shortuuid
from werkzeug.security import generate_password_hash,check_password_hash
import enum
from datetime import datetime
class GenderEnum(enum.Enum):
    MALE=1
    FEMALE=2
    SECRET=3
    UNKNOWN=4
class FrontUser(db.Model):
    __tablename__='front_user'
    id=db.Column(db.String(100),primary_key=True,default=shortuuid.uuid)
    telephone=db.Column(db.String(11),nullable=False,unique=True)
    username=db.Column(db.String(50),nullable=False)
    _password=db.Column(db.String(200),nullable=False)
    email=db.Column(db.String(50),unique=True)
    realname=db.Column(db.String(50))
    avatar=db.Column(db.String(100))
    signature=db.Column(db.String(100))
    gender=db.Column(db.Enum(GenderEnum),default=GenderEnum.UNKNOWN)
    join_time=db.Column(db.DateTime,default=datetime.now)
    def __init__(self,*args,**kwargs):
        if "password" in kwargs:
            self.password = kwargs.get("password")
            kwargs.pop("password")
        super(FrontUser,self).__init__(*args,**kwargs)
    @property
    def password(self):
        return self._password
    @password.setter
    def password(self,newpwd):
        self._password=generate_password_hash(newpwd)
    def check_password(self,rawpwd):
        return check_password_hash(self._password,rawpwd)
#3.在commands.py文件里面创建命令行创建前台用户对象,并添加到app中
from apps.front import models as front_models
FrontUser=front_models.FrontUser
@click.option("-t","--telephone",required=True)
@click.option("-u","--username",required=True)
@click.option("-p","--password",required=True)
def create_front_user(telephone,username,password):
    user=FrontUser(telephone=telephone,username=username,password=password)
    db.session.add(user)
    db.session.commit()
app:app.cli.command("create_front_user")(commands.create_front_user)
#4.migrate和upgrade数据库,并cmd添加前台用户
flask db migrate
flask db upgrade
flask create_front_user -t 10086 -u 中国移动 -p 111111

2.2.前台用户注册

#1.写注册的get页面front_signup.html
{% from 'common/_macros.html' import static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>front论坛注册页面</title>
    <script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
    <link href="http://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <script src="http://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <style>
        body{background: #f3f3f3;}
        .outer-box{
            width: 854px;
            background: #fff;
            margin: 0 auto;
            overflow: hidden;
        }
        .logo-box{
            text-align: center;
            padding-top: 40px;
        }
        .logo-box img{
            width: 60px;
            height: 60px;
        }
        .page-title{
            text-align: center;
        }
        .sign-box{
            width:300px;
            margin:0 auto;
            padding-top:50px;
        }
    </style>
</head>
<body>
    <div class="outer-box">
        <div class="logo-box">
            <a href="/">
                <img src="{{static('common/pic/front.ico')}}" alt="">
            </a>
    </div>
    <h2 class="page-title">论坛账号注册</h2>
    <div class="sign-box">
        <div class="form-group">
            <div class="input-group">
                <input type="text" class="form-control" name="telephone" placeholder="手机号码">
                <span class="input-group-btn">
                    <button id="sms-captcha-btn" class="btn btn-default">发送验证码</button>
                </span>
            </div>
        </div>

        <div class="form-group">
            <input type="text" class="form-control" name="sms_captcha" placeholder="短信验证码">
        </div>

        <div class="form-group">
            <input type="text" class="form-control" name="username" placeholder="用户名">
        </div>

        <div class="form-group">
            <input type="password" class="form-control" name="password" placeholder="密码">
        </div>

        <div class="form-group">
            <input type="password" class="form-control" name="password2" placeholder="确认密码">
        </div>

        <div class="form-group">
            <div class="input-group">
                <input type="text" class="form-control" name="graph_captcha" placeholder="图形验证码">
                <span class="input-group-addon">图形验证码</span>
            </div>
        </div>
        
        <div class="form-group">
            <button id="submit-btn" class="btn btn-warning btn-block">立即注册</button>
        </div>
    </div>
    </div>
</body>
</html>
#2.在apps/front/views.py里面写注册路由
from flask import Blueprint,views,render_template
bp=Blueprint('front',__name__)
class SignupView(views.MethodView):
    def get(self):
        return render_template('/front/front_signup.html')
bp.add_url_rule('/signup/',view_func=SignupView.as_view('signup'))

2.3.前台用户注册的图形验证码

pip install pillow==9.4.0
#1.在views.py中写验证码视图函数
from utils.captcha import Captcha
from io import BytesIO
@bp.route('/captcha/')
def graph_captcha():
    text,image=Captcha.gene_graph_captcha()
    out=BytesIO()
    image.save(out,'png')
    out.seek(0)
    resp=make_response(out.read())
    resp.content_type='image/png'
    return resp
#2.把图形验证码的视图映射到html的src上
<img id="captcha-img" class='captcha-img' src="{{url_for('front.graph_captcha')}}">
#3.每次点(小手功能:cursor: pointer)验证码,更换图片,这里要用js来做
$(function (){
    $('#captcha-img').click(function(event){
        var self=$(this);
        var src=self.attr('src');
        var newsrc=zlparam.setParam(src,'xx',Math.random());
        self.attr('src',newsrc);
    });
});
#4.将js和css加载到html中
<script src="{{static('common/js/zlparam.js')}}"></script>
<script src="{{static('front/js/front_signup.js')}}"></script>
<link rel="stylesheet" href="{{static('front/css/front_signup.css')}}">

2.3.前台用户注册的短信验证码

用到了阿里大于第三方服务商,进去之后创建短信验证签名和模版。

#1.在config文件里设置
ALIDAYU_APP_KEY=''
ALIDAYU_APP_SECRET=''
ALIDAYU_SIGN_NAME=''
ALIDAYU_TEMPLATE_CODE=''
#2.在exts里面实例化
from utils.alidayu import AlidayuAPI
alidayu=AlidayuAPI()
#3.在app里面绑定
from exts import alidayu
alidayu.init_app(app)
#4.在common/views里面写发送验证码的视图函数,即发送功能的后端
from flask import Blueprint,request
from exts import alidayu
from utils import restful
from utils.captcha import Captcha
bp=Blueprint('common',__name__,url_prefix='/c')
@bp.route('/sms_captcha/')
def sms_captcha():
    telephone=request.args.get('telephone')
    if not telephone:
        return restful.paramserror(message="请传入手机号码")
    captcha=Captcha.gene_text(number=4)
    if alidayu.send_sms(telephone,code=captcha):
        return restful.success()
    else:
        return restful.paramserror(message="短信验证码发送失败")
#5.在front_signup.js里面写点击发送短信验证码后的js相应
$(function(){
    $("#sms-captcha-btn").click(function(event){
        event.preventDefault();
        var self=$(this);
        var telephone=$("input[name='telephone']").val();
        if (!(/^1[345789]\d(9)$/.test(telephone))){
            zlalert.alertInfoToast("请输入正确的手机号码");
            return;
        }
        zlajax.get({
            'url':'/c/sms_captcha?teltphone='+telephone,
            'success':function(data){
                if (data['code']==200){
                    zlalert.alertSuccessToast("短信验证码发送成功");
                    self.attr("disabled",'disabled');
                    var timeCount=60;
                    var timer=setInterval(function(){
                        timeCount--;
                        self.text(timeCount);
                        if (timeCount<=0){
                            self.removeAttr('disabled');
                            clearInterval(timer);
                            self.text("发送验证码");
                        }
                    },1000);
                }else{
                    zlalert.alertInfoToast(data['message']);
                }
            }
        });
    });
});
#6.js里面用到了alert和ajax,所以给html里面添加
    <script src="{{static('common/js/zlajax.js')}}"></script>
    <link rel="stylesheet" href="{{static('common/js/sweetalert/sweetalert.css')}}">
    <script src="{{static('common/js/sweetalert/sweetalert.min.js')}}"></script>
    <script src="{{static('common/js/sweetalert/zlalert.js')}}"></script>

js接口用post方式,并采用md5加密,并对代码进行加密混淆

#1.在form.py中对post提交的手机号和时间戳以及salt进行表单验证,并对md5加密后的值进行验证。
from wtforms import Form,StringField
from wtforms.validators import regexp,InputRequired
import hashlib
class BaseForm(Form):
    def get_error(self):
        message=self.errors.popitem()[1][0]
        return message
    def validate(self):
        return super(BaseForm,self).validate()
class SMSCaptchaForm(BaseForm):
    telephone=StringField(validators=[regexp(r'1[345789]\d{9}')])
    timestamp=StringField(validators=[regexp(r'\d{13}')])
    salt='adfgervggrtbetynbwrbg#$%@$^@'
    sign=StringField(validators=[InputRequired()])
    def Validate(self):
        result=super(SMSCaptchaForm,self).validate()
        if not result:
            return False
        telephone=self.telephone.data
        timestamp=self.timestamp.data
        sign=self.sign.data
        sign2=hashlib.md5((timestamp+telephone+self.salt).encode('utf-8')).hexdigest()#md5加密必须传bytes类型的数据
        if sign ==sign2:
            return True
        else:
            return False
#2.在views.py中采用post方式访问视图,并加入md5加密
from .forms import SMSCaptchaForm
@bp.route('/sms_captcha/',methods=['POST'])
def sms_captcha():
    form =SMSCaptchaForm(request.form)
    if form.validate():
        telephone=form.telephone.data
        captcha=Captcha.gene_text(number=4)
        if alidayu.send_sms(telephone,code=captcha):
            return restful.success()
        else:
            return restful.paramserror()
    else:
        return restful.paramserror(message='参数错误')
#3.fron_signup.js里面写post提交tele,time和sign的加密表单的前端
$(function(){
    $("#sms-captcha-btn").click(function(event){
        event.preventDefault();
        var self=$(this);
        var telephone=$("input[name='telephone']").val();
        if (!(/^1[345879]\d{9}$/.test(telephone))){
            print(telephone)
            zlalert.alertInfoToast('请输入正确的手机号码!');
            return;
        }
        var timestamp=(new Date).getTime();
        var sign=md5(timestamp+telephone+'adfgervggrtbetynbwrbg#$%@$^@')
        zlajax.post({
            'url':'/c/sms_captcha',
            'data':{
                'telephone':telephone,
                'timestamp':timestamp,
                'sign':sign
            },
            'success':function(data){
                if (data['code']==200){
                    zlalert.alertSuccessToast("短信验证码发送成功");
                    self.attr("disabled",'disabled');
                    var timeCount=60;
                    var timer=setInterval(function(){
                        timeCount--;
                        self.text(timeCount);
                        if (timeCount<=0){
                            self.removeAttr('disabled');
                            clearInterval(timer);
                            self.text("发送验证码");
                        }
                    },1000);
                }else{
                    zlalert.alertInfoToast(data['message']);
                }
            }
        });
    });
});
#4.js里面用到md5加密,所以在html里面倒入
<script src="https://cdn.bootcss.com/blueimp-md5/2.10.0/js/md5.min.js"></script>
#5.js采用了post请求,所以要在html中加入crsf的token
<meta  name="csrf-token" content="{{csrf_token()}}">

对js代码用js加密工具进行先加密再混淆压缩。
服务器缓存验证码。

#1.短信验证码保存到cache中,在views视图中,当验证码发送成功时,缓存到服务器
from utils import zlcache
zlcache.set(telephone,captcha)
#2.图形验证码
zlcache.set(text.lower(),text.lower())

2.4用容联云完成短信验证

上面的短信验证我没成功,所以又用容联云来做短信验证功能。

#1.pip install ronglian_sms_sdk
#2.在common/views.py里面
from ronglian_sms_sdk import SmsSDK
import json
import string,random
accId = 'xxx'
accToken = 'xxx'
appId = 'xxx'
sdk = SmsSDK(accId, accToken, appId)
tid="1"
from .forms import SMSCaptchaForm
@bp.route('/sms_captcha/',methods=['POST'])
def sms_captcha():
    form =SMSCaptchaForm(request.form)
    if form.validate():
        telephone=form.telephone.data
        source=string.digits*4
        captcha=random.sample(source,4)
        captcha=''.join(captcha)
        if sdk.sendMessage(tid, telephone, [captcha]):
            zlcache.set(telephone,captcha)
            return restful.success()
        else:
            return restful.paramserror()
    else:
        return restful.paramserror(message='参数错误')

2.5.前台用户注册前后端

ajax方式post表单

#1.front_signup.js里面写post提交前端
$(function() {
    $("#submit-btn").click(function(event){
        event.preventDefault();
        var telephone_input = $("input[name='telephone']");
        var sms_captcha_input = $("input[name='sms_captcha']");
        var username_input= $("input[name='username']");
        var password1_input= $("input[name='password']");
        var password2_input= $("input[name='password2']");
        var graph_captcha_input =$("input[name='graph_captcha']");
        var telephone=telephone_input.val();
        var sms_captcha=sms_captcha_input.val();
        var username=username_input.val();
        var password1=password1_input.val();
        var password2=password2_input.val();
        var graph_captcha=graph_captcha_input.val();
        zlajax.post({
            'url':'/signup/',
            'data': {'telephone':telephone,
                    'sms_captcha':sms_captcha,
                    'username':username,
                    'password1':password1,
                    'password2':password2,
                    'graph_captcha':graph_captcha
                    },
            'success':function(data){
                if(data['code']==200){
                    window.location='/';
                }else{
                    zlalert.alertInfo(data['message']);
                }
            },
            'fail':function(data){
                zlalert.alertNetworkError();
            }
        });
    });
});
#2.后端首先需要在form.py里面进行表单验证
from wtforms import Form,StringField
from wtforms.validators import Regexp,EqualTo,ValidationError
from utils import zlcache
class BaseForm(Form):
    def get_error(self):
        message=self.errors.popitem()[1][0]
        return message
    def validate(self):
        return super(BaseForm,self).validate()
class SignupForm(BaseForm):
    telephone=StringField(validators=[Regexp(r"1[345789]\d{9}",message="请输入正确格式的手机号码")])
    sms_captcha=StringField(validators=[Regexp(r"\w{4}",message="请输入正确格式的短信验证码")])
    username=StringField(validators=[Regexp(r".{2,20}",message="请输入正确格式的用户名")])
    password1=StringField(validators=[Regexp(r"[0-9a-zA-Z_\.]{6,20}",message="请输入正确格式的密码")])
    password2=StringField(validators=[EqualTo("password1",message="两次密码不一致")])
    graph_captcha=StringField(validators=[Regexp(r"\w{4}",message="请输入正确格式的图形验证码")])
    def validate_sms_captcha(self,field):
        sms_captcha=field.data
        telephone=self.telephone.data
        sms_captcha_mem=zlcache.get(telephone)
        if not sms_captcha_mem or sms_captcha_mem.lower()!=sms_captcha.lower():
            raise ValidationError(message="短信验证码错误")
    def validate_graph_captcha(self,field):
        grahp_captcha=field.data
        grahp_captcha_mem=zlcache.get(grahp_captcha.lower())
        if not grahp_captcha_mem :
            raise ValidationError(message="图形验证码错误")
#3.在vies.py里面完成SignupView视图函数的post方法
def post(self):
        form = SignupForm(request.form)
        if form.validate():
            telephone=form.telephone.data
            username=form.username.data
            password=form.password1.data
            user=FrontUser(telephone=telephone,username=username,password=password)
            db.session.add(user)
            db.session.commit()
            return restful.success()
        else:
            print(form.get_error())
            return restful.paramserror(form.get_error())

2.6.跳转回上一个页面

每一个view视图函数都可以通过request.referrer拿到上一个页面的地址信息。

#1.在SignupView视图类的get函数里面加入
    def get(self):
        return_to=request.referrer
        if return_to and return_to!=request.url and safeutils.is_safe_url(return_to):
            return render_template('/front/front_signup.html',return_to=return_to)
        else:
            return render_template('/front/front_signup.html')
#2.在front_signup.html的表单body里面加入,将return——to传到前端
<span style="display: none;" id="return-to-span">{{return_to}}</span>
#3.在fron_signup.js里面拿到return——to,并判断跳转页面
'success':function(data){
                if(data['code']==200){
                    var return_to=$("#return-to-span").text();
                    if(return_to){
                        window.location=return_to;
                    }else{window.location='/';}
                    
                }else{
                    zlalert.alertInfo(data['message']);
                }
            },

2.7.登陆功能

get

#1.在front_signin.html中写视图模版
{% extends 'front/front_signbase.html' %}
{% from 'common/_macros.html' import static %}
{%block title%}论坛账号登录{%endblock%}
{%block head%}

{%endblock%}
{%block h2_block%}论坛登录{%endblock%}
{%block signbox%}
    <div class="form-group">
        <input type="text" class="form-control" name="telephone" placeholder="手机号码">
    </div>
    <div class="form-group">
        <input type="password" class="form-control" name="password" placeholder="密码">
    </div>
    <div class="checkbox">
        <label>
            <input type="checkbox" name="remember" value="1">记住我
        </label>
    </div>
    <div class="form-group ">
        <button class="btn btn-warning btn-block" id="submit">立即登录</button>
    </div>
    <div class="form-group">
        <a href="{{url_for('front.signup')}}" class="signup-link">没有账号?立即注册</a>
        <a href="#" class="resetpwd-link" style="float:right;">忘记密码?重新找回</a>
    </div>
{%endblock%}
#2.在front/views.py中写视图函数
class SigninView(views.MethodView):
    def get(self):
        return_to=request.referrer
        if return_to and return_to!=request.url and return_to!=url_for('front.signup') and safeutils.is_safe_url(return_to):
           return render_template('front/front_signin.html',return_to=return_to)
        else: 
            return render_template('front/front_signin.html')
bp.add_url_rule("/signin/",view_func=SigninView.as_view('signin'))

post

#1.forms.py表单验证
class SigninForm(BaseForm):
    telephone=StringField(validators=[Regexp(r"1[345789]\d{9}",message="请输入正确格式的手机号码")])
    password=StringField(validators=[Regexp(r"[0-9a-zA-Z_\.]{6,20}",message="请输入正确格式的密码")])
    remember=StringField()
#2.在vies.py中完成表单post提交
from .forms import SigninForm
import config
def post(self):
        form=SigninForm(request.form)
        if form.validate():
            telephone=form.telephone.data
            password=form.password.data
            remember=form.remember.data
            user=FrontUser.query.filter_by(telephone=telephone).first()
            if user and user.check_password(password):
                session[config.FRONT_USER_ID]=user.id
                if remember:
                    session.permanent=True
                return restful.success()
            else:
                return restful.paramserror(message="手机号或密码错误")
        else:
            return restful.paramserror(message=form.get_error())
#3.在fron_signin.js中进行ajax提交表单
$(function() {
$("#submit").click(function(event){
    event.preventDefault();
    var telephone_input=$("input[name='telephone']");
    var password_input=$("input[name='password']");
    var remember_input=$("input[name='remember']");
    var telephone=telephone_input.val();
    var password=password_input.val();
    var remember=remember_input.checked?1:0;
    zlajax.post({
        'url':'/signin/',
        'data':{
            'telephone':telephone,
            'password':password,
            'remember':remember
        },
        'success':function(data){
            if(data['code']==200){
                var return_to=$("#return-to-span").text();
                if (return_to){
                    window.location=return_to;
                }else{
                    window.location="/";
                }
            }else{
                zlalert.alertInfo(data['message']);
            }
        }
    });
});
});
#4.在fron_signin.html中将js添加进
<script src="{{static('front/js/front_signin.js')}}"></script>

2.8.内部导航条的抽取和轮播图

导航条:在bootstrap的v3中文文档,组件,导航条,里面复制并修改为自己的应用。
轮播图:在bootstrap的v3中文文档,JavaScript 插件,carousel,复制并修改。

#1.在fron_base.css里面设置版芯,大版心和小版心
.main-container{
    width:990px;
    margin:0 auto;
    overflow:hidden;
}
.lg-container{
    width: 730px;
    float: left;
}
.sm-container{
    width: 250px;
    float: right;
}
#2.在front_index.css中设置轮播和图片的宽
.index-banner{
    border-radius: 10px;
    overflow: hidden;
    height:200px;
}
.index-banner img{
    height: 200px;
}
#3.css导入到fron_base.html

2.9前台轮播图的显示

#1.首先将后台管理的轮播图按优先级排序
@bp.route("/banners/")#处理轮播图显示的视图
@login_required
def banners():
    banners=BannerModel.query.order_by(BannerModel.priority.desc()).all()
    return render_template("cms/cms_banners.html",banners=banners)
#2.通过前台的front/views.py将banners的数据传递到html前端
from ..models import BannerModel
@bp.route('/')
def index():
    banners=BannerModel.query.order_by(BannerModel.priority.desc()).limit(4)
    context={"banners": banners}
    return render_template("/front/front_index.html",**context)
#3.前端front_index.html 接收参数并显示
 <div class="carousel-inner" role="listbox">
              {%for banner in banners%}
                {% if loop.first %}
                <div class ="item active">
                {% else %}
                <div class="item">
                {% endif %}
                    <a href="{{banner.link_url}}" >
                      <img src="{{banner.image_url}}" alt="...">
                    </a>
                </div>
              {% endfor %}
            </div>

2.10前台板块的显示

#1.首先在front/views.py里面将数据库的板块信息传递给前端
from ..models import BannerModel,BoardModel
bp=Blueprint('front',__name__)
@bp.route('/')
def index():
    banners=BannerModel.query.order_by(BannerModel.priority.desc()).limit(4)
    boards=BoardModel.query.all()
    context={"banners": banners,'boards': boards}
#2.在front_index.html的sm-container里面写板块的渲染
    <div class="sm-container">
      <div style="padding-bottom: 10px;">
        <button class="btn btn-warning btn-block" >发布帖子</button>
      </div>
      <div class="list-group">
        <a href="#" class="list-group-item active">所有板块</a>
        {% for board in boards %}
        <a href ="#" class="list-group-item">{{board.name}}</a>
        {% endfor %}
      </div>
    </div>

2.11使用ueditor实现富文本

1.上传服务器本地image目录下

#1.在config文件中设置上传路径
import os
UEDITOR_UPLOAD_PATH =os.path.join(os.path.dirname(__file__),'image')
#2.导入editor.py并绑定到蓝图上
from flask import Flask,render_template
from ueditor import bp
import config
app=Flask(__name__)
app.config.from_object(config)
app.register_blueprint(bp)
@app.route('/')
def index():
    return render_template('ueditor_index.html')
if __name__ == '__main__':
    app.run(debug=True)
#3.ueditor_index.html中设置服务器域名
<!DOCTYPE HTML>
<html lang="en-US">
<head>
	<meta charset="UTF-8">
    	<!-- 配置文件 -->
	<script type="text/javascript" src="{{url_for('static',filename='ueditor/ueditor.config.js')}}"></script>
	<!-- 编辑器源码文件 -->
	<script type="text/javascript" src="{{url_for('static',filename='ueditor/ueditor.all.js')}}"></script>
	<title>ueditor demo</title>
</head>
<body>
	<script id='editor' type="text/plain"></script>
    <script>
        	var ue = UE.getEditor('editor',{'serverUrl':'/ueditor/upload'});
    </script>
</body>
</html>

2.上传到七牛

#1.在config.py中设置七牛,就可以成功上传
UEDITOR_UPLOAD_TO_QINIU = True
UEDITOR_QINIU_ACCESS_KEY = ""
UEDITOR_QINIU_SECRET_KEY = ""
UEDITOR_QINIU_BUCKET_NAME = ""
UEDITOR_QINIU_DOMAIN = ""

2.12发布帖子

1.发布帖子的后端逻辑

#1.在apps/models.py里面创建帖子模型,并与board进行一对多的关系映射
class PostModel(db.Model):
    __tablename__='post'
    id=db.Column(db.Integer,primary_key=True,autoincrement=True)
    title=db.Column(db.String(200),nullable=False)
    content=db.Column(db.Text,nullable=False)
    create_time=db.Column(db.DateTime,default=datetime.now)
    board_id=db.Column(db.Integer,db.ForeignKey("board.id"))
    author_id=db.Column(db.String(100),db.ForeignKey("front_user.id"),nullable=False)
    board=db.relationship("BoardModel",backref="posts")
    author=db.relationship("FrontUser",backref="posts")
#2.将模型映射到数据库
flask db migrate
flask db upgrade
#3.在decorates.py写一个前台用户登录验证器
from flask import session,redirect,url_for,g
from functools import wraps
import config
def login_required(func):
    @wraps(func)
    def inner(*args, **kwargs):
        if config.FRONT_USER_ID in session:
            return func(*args, **kwargs)
        else:
            return redirect(url_for('front.signin'))
    return inner
#4.在front/forms.py里面写帖子提交时的表单验证
class AddPostForm(BaseForm):
    title=StringField(validators=[InputRequired(message='请输入标题')])
    content=StringField(validators=[InputRequired(message='请输入内容')])
    board_id=IntegerField(validators=[InputRequired(message='请输入板块id')])
#5.在front/views.py里面写发布帖子的视图函数
from .decorates import login_required
from .forms import AddPostForm
from ..models import PostModel
@bp.route('/apost/',methods=['GET','POST'])
@login_required
def apost():
    if request.method=='GET':
        return render_template('front/front_apost.html')
    else:
        form=AddPostForm(request.form)
        if form.validate():
            title=form.title.data
            content=form.content.data
            board_id=form.board_id.data
            board=BoardModel.query.get(board_id)
            if not board:
                return restful.paramserror(message='没有这个板块')
            post=PostModel(title=title,content=content)
            post.board=board
            post.author=g.front_user
            db.session.add(post)
            db.session.commit()
            return restful.success()
        else:
            return restful.paramserror(message=form.get_errot())
#6.在app.py里面倒入ueditor并注册到蓝图
from apps.ueditor import bp as ueditor_bp
app.register_blueprint(ueditor_bp)
#7.在front/front_apost.html里面写发布帖子的前端
{% extends "front/front_base.html" %}
{% block title %}
发布帖子
{% endblock %}
{% block head %}
    <script src="{{url_for('static',filename='ueditor/ueditor.config.js')}}"></script>
    <script src="{{url_for('static',filename='ueditor/ueditor.all.min.js')}}"></script>
    <script src="{{url_for('static',filename='front/js/front_apost.js')}}"></script>
{% endblock %}
{% block body %}
<form action="" method="post">
    <div class="form-group">
        <div class="input-group">
            <span class="input-group-addon">标题</span>
            <input type="text" class="form-control" name="title">
        </div>
    </div>
    <div class="form-group">
        <div class="input-group">
            <span class="input-group-addon">板块</span>
            <select name="board_id" class="form-control">
                {% for board in boards %}
                    <option value="{{board.id}}">{{board.name}}</option>
                {% endfor %}
            </select>
        </div>
    </div>
    <div class="form-group">
        <script id="editor" type="text/plain" style="height: 500px;"></script>
    </div>
    <div class="form-group">
        <button class="btn btn-danger" id="submit-btn">>发布帖子</button>
    </div>
</form>
{% endblock%}
#8.在front_apost.js里面写发布帖子的前端ueditor的服务器地址
$(function(){
    var ue=UE.getEditor('editor',{
        'serverUrl':'/ueditor/upload/'
    });
});
#9.在front_index.html里面写发布帖子的跳转链接
<a href="{{url_for('front.apost')}}" class="btn btn-warning btn-block" >发布帖子</a>

2.给front用户一个hook

在front/hooks.py中
from .views import bp 
from .models import FrontUser
from flask import session,g
import config
@bp.before_request
def my_before_request():
    if config.FRONT_USER_ID in session:
        user_id=session.get(config.FRONT_USER_ID)
        user=FrontUser.query.get(user_id)
        if user:
            g.front_user=user
#2.在front/__init__.py中
from . import hooks
#3.在front_base.html中加入用户信息
<ul class="nav navbar-nav navbar-right">
                  {% if front_user %}
                    <li class="dropdown">
                      <a class="dropdown-toggle" data-toggle="dropdown" href="#" type="button" id="dropdownMenul" aria-haspopup="true" aria-expanded="true">{{g.front_user.username}}<span class='caret'></span></a>
                      <ul class="dropdown-menu" aria-labelledby="dropdownMenul">
                        <li ><a href="#">个人中心</li>
                        <li ><a href="#">设置</li>
                        <li ><a href="#">注销</li>
                      </ul>
                    </li>
                  {% else %}
                    <li><a href="{{url_for('front.signup')}}">注册</a></li>
                    <li><a href="{{url_for('front.signin')}}">登录</a></li>
                  {% endif %}
                </ul>

3.发布帖子按钮功能实现

#1.在front_apost.js中监控按钮
$(function(){
    var ue=UE.getEditor('editor',{
        'serverUrl':'/ueditor/upload/'
    });
    $("#submit-btn").click(function(event){
        event.preventDefault();
        var titleInput=$("input[name='title']");
        var boardSelect=$("select[name='board_id']");
        var title=titleInput.val()
        var board_id=boardSelect.val();
        var content=ue.getContent();
        zlajax.post({
            'url':'/apost/',
            'data':{
                'title':title,
                'content':content,
                'board_id':board_id
            },
            'success':function(data){
                if (data['code']==200){
                    zlalert.alertConfirm({
                        'msg':'恭喜,帖子发表成功',
                        'cancelText':'回到首页',
                        'confirmText':'再发一篇',
                        'cancelCallback':function(){
                            window.location='/';},
                        'confirmCallback':function(){
                            titleInput.val("");
                            ue.setContent("")}
                    });
                }else{
                    zlalert.alertInfo(data['message']);
                }
            }
        });
    });
});

2.13首页帖子列表

#1.在front的views.py里面传递帖子参数
posts=PostModel.query.all()
context={"banners": banners,'boards': boards,'posts': posts}
#2.在front_index.html里面的<div class="lg-container">写帖子列表
<div class="post-group">
  <ul class="post-group-head">
    <li class="active"><a href="#">最新</a></li>
    <li><a href="#">精华帖子</a></li>
    <li><a href="#">点赞最多</a></li>
    <li><a href="#">评论最多</a></li>
  </ul>
  <ul class="post-list-group">
    {%for post in posts%}
    <li>
      <div class="author-avatar-group">
        <img src="{{post.author.avatar or url_for('static',filename='common/pic/front.png' )}}" alt="">
      </div>
      <div class="post-info-group">
        <p class="post-title">{{post.title}}</p>
        <p class="post-info">
          <span>作者:{{post.author.username}}</span>
          <span>发表时间:{{post.create_time}}</span>
          <span>评论:0</span>
          <span>阅读:0</span>
        </p>
      </div>
    </li>
    {% endfor %}
  </ul>
</div>
#3.front_index.css里面写样式
.post-group{
    border:1px solid #ddd;
    margin-top: 20px;
    overflow: hidden;
    border-radius: 5px;
    padding:10px
}
.post-group-head{
    overflow: hidden;
    list-style: none;
}
.post-group-head li{
    float: left;
    padding: 5px 10px;
}
.post-group-head li a{
    color: #333;
}
.post-group-head li.active{
    background:#ccc;
}
.post-list-group{
    margin-top: 20px;
}
.author-avatar-group img{
    float: left;
    width: 50px;
    height:50px;
    border-radius: 50%;
    clear: both;
    margin-top: 5px;
}
.post-info-group{
    float: left;
    margin-left: 10px;
    border-bottom: 1px solid #e6e6e6;
    width:90%;
    padding-bottom: 10px;
}
.post-info-group .post-info{
    margin-top: 10px;
    font-size: 12px;
    color: #8c8c8c;
}
.post-info span{
    margin-right: 10px;
}
#4.在front_base.css里面写消除不同浏览器设置的样式
a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote, body, canvas, caption, cite, code, dd, del, details, dfn, div, dl, dt, em, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, html, i, iframe, img, ins, kbd, label, legend, li, mark, menu, nav, object, ol, output, p, pre, q, ruby, s, samp, section, small, span, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, time, tr, tt, u, ul, var, video {
    margin: 0;
    padding: 0;
    border: 0;
    list-style: none;
    vertical-align: baseline;
}

2.14帖子分页

使用到了flask_paginate

#1.pip install flask-paginate
#2.在config里面设置一页多少篇
PER_PAGE=2
#3.在front/views.py里面的首页试图函数里面实例化paginate
from flask_paginate import Pagination
bp=Blueprint('front',__name__)
@bp.route('/')
def index():
    banners=BannerModel.query.order_by(BannerModel.priority.desc()).limit(4)
    boards=BoardModel.query.all()
    # posts=PostModel.query.all()
    page=request.args.get(get_page_parameter(),type=int,default=1)
    start=(page-1)*config.PER_PAGE
    end=start+config.PER_PAGE
    posts=PostModel.query.slice(start,end)
   	pagination=Pagination(bs_version=3,page=page,total=PostModel.query.count())
    context={"banners": banners,'boards': boards,'posts': posts,'pagination':pagination}
    return render_template("/front/front_index.html",**context)
#4.在front_index.html里面写翻页
<div style="text-align:center;">
              {{pagination.links}}
            </div>

2.14帖子根据板块过滤显示

#1.在front_index.html的sm-container里面设置点击板块后跳转首页,并传递bd板块id信息到视图函数
<div class="list-group">
        {% if current_board %}
          <a href="/" class="list-group-item ">所有板块</a>
        {% else %}
          <a href="/" class="list-group-item active">所有板块</a>
        {% endif %}
        {% for board in boards %}
        {% if board.id==current_board %}
          <a href ="{{url_for('front.index',bd=board.id)}}" class="list-group-item active">{{board.name}}</a>
        {% else %}
          <a href ="{{url_for('front.index',bd=board.id)}}" class="list-group-item">{{board.name}}</a>
        {% endif %}
          {% endfor %}
      </div>
#2.在视图函数里接收板块id信息并返回给front_index.html去显示
bp=Blueprint('front',__name__)
@bp.route('/')
def index():
    board_id = request.args.get('bd',type=int,default=None)
    banners=BannerModel.query.order_by(BannerModel.priority.desc()).limit(4)
    boards=BoardModel.query.all()
    # posts=PostModel.query.all()
    page=request.args.get(get_page_parameter(),type=int,default=1)
    start=(page-1)*config.PER_PAGE
    end=start+config.PER_PAGE
    posts=None
    total=0
    if board_id:
        query_obj=PostModel.query.filter_by(board_id=board_id)
        posts=query_obj.slice(start,end)
        total=query_obj.count()
    else:
        posts=PostModel.query.slice(start,end)
        total=PostModel.query.count()
    pagination=Pagination(bs_version=3,page=page,total=total,outer_window=0,inner_window=1)
    context={"banners": banners,'boards': boards,'posts': posts,'pagination':pagination,'current_board': board_id}
    return render_template("/front/front_index.html",**context)

2.15帖子详情页

#1.在hooks和front_404.html里面写一个对404页面的处理
@bp.errorhandler
def page_not_found():
    return render_template('front/front_404.html'),404
<!DOCTYPE html>
<html lang="en" >
    <head>
        <meta charset="utf-8" />
        <title>bbs_404</title>
    </head>
    <body>
        您要找的页面已经到火星
        <div>
            <a href="/">返回首页</a>
        </div>
    </body>
</html>
#2.在front_index.html里面写点击帖子标题后的跳转视图函数
<p class="post-title">
 <a href="{{url_for('front.post_detail',post_id=post.id)}}">{{post.title}}</a>
</p>
#3.在views里面写视图函数
from flask import abort
@bp.route('/p/<post_id>/')
def post_detail(post_id):
    post=PostModel.query.get(post_id)
    if not post:
        abort(404)
    return render_template('front/front_pdetail.html',post=post)
#4.在front_pdetail.html和front_pdetail.css里面写帖子详情页
{% extends 'front/front_base.html' %}
{% block title %}
    {{post.title}}
{% endblock %}
{% block head %}
    <link rel="stylesheet" href="{{url_for('static',filename='front/css/front_pdetail.css')}}">
{% endblock %}
{% block body %}
    <div class="lg-container">
        <div class="post-container">
            <h2>{{post.title}}</h2>
            <p class="post-info-group">
                <span>发表时间:{{post.create_time}}</span>
                <span>作者:{{post.author.username}}</span>
                <span>所属板块:{{post.board.name}}</span>
                <span>阅读数:{{post.read_count}}</span>
                <span>评论数:0</span>
            </p>
            <article class="post-content">
                {{post.content|safe}}
            </article>
    </div>
    <div class="sm-container"></div>
{%endblock%}
.post-container{
    border:1px solid #e6e6e6;
    padding: 10px;
}
.post-info-group{
    font-size: 12px;
    color:#8c8c8c;
    border-bottom: 1px solid #e6e6e6;
    margin-top: 20px;
    padding-bottom: 10px;
}
.post-info-group span{
    margin-left: 20px;
}
.post-content{
    margin-top: 20px;
}
.post-content img{
    max-width: 100%;
}

2.16帖子评论功能

#1.apps/models.py里面创建评论模型,并migrate和upgrade
class CommentModel(db.Model):
    __tablename__ = "comment"
    id=db.Column(db.Integer,primary_key=True,autoincrement=True)
    content=db.Column(db.Text,nullable=False)
    create_time=db.Column(db.DateTime,default=datetime.now)
    post_id=db.Column(db.Integer,db.ForeignKey("post.id"))
    author_id=db.Column(db.String(100),db.ForeignKey("front_user.id"),nullable=False)
    post=db.relationship('PostModel',backref='comments')
    author=db.relationship("FrontUser",backref="comments")    
#2.form表单验证
class AddCommentForm(BaseForm):
    content=StringField(validators=[InputRequired(message='请输入评论内容')])
    post_id=IntegerField(validators=[InputRequired(message='请输入帖子id')])
#3.添加帖子评论的视图函数
from ..models import CommentModel
from .forms import AddCommentForm
@bp.route('/acomment/',methods=['POST'])
@login_required
def add_comment():
    form=AddCommentForm(request.form)
    if form.validate():
        content=form.content.data
        post_id=form.post_id.data
        post=PostModel.query.get(post_id)
        if post:
            comment=CommentModel(content=content)
            comment.post=post
            comment.author=g.front_user
            db.session.add(comment)
            db.session.commit()
            return restful.success()
        else:
            return restful.paramserror('没有这篇帖子')
    else:
        return restful.paramserror(form.get_error())
#4.在front_pdetail.html和front_pdetail.css里面写评论的显示和添加评论的入口按钮
<script src="{{url_for('static',filename='ueditor/ueditor.config.js')}}"></script>
<script src="{{url_for('static',filename='ueditor/ueditor.all.js')}}"></script>
<script src="{{url_for('static',filename='front/js/front_pdetail.js')}}"></script>
 <div class="comment-group">
            <h3>评论列表</h3>
            <ul class="comment-list-group">
                {% for comment in post.comments %}
                    <li>
                        <div class="avatar-group">
                            <img src="{{comment.author.avatar or url_for('static',filename='/common/pic/front.png')}}" alt="">
                        </div>
                        <div class="comment-content">
                            <p class="author-info">
                                <span>{{comment.author.username}}</span>
                                <span>{{comment.create_time}}</span>
                            </p>
                            <p class="comment-txt">
                                {{comment.content|safe}}
                            </p>
                        </div>
                    </li>
                {% endfor %}
            </ul>
        </div>
        <div class="add-comment-group">
            <h3>发表评论</h3>
            <script id="editor" type="text/plain" style="height:100px;"></script>
            <div class="comment-btn-group">
                <button class="btn btn-primary" id="comment-btn">发表评论</button>
            </div>
        </div>
  .comment-group{
    margin-top: 20px;
    border:1px solid #e8e8e8;
    padding:10px;
}
.add-comment-group{
    margin-top: 20px;
    padding:10px;
    border:1px solid #e8e8e8;
}
.add-comment-group h3{
    margin-bottom: 10px;
}
.comment-btn-group{
    margin-top: 10px;
    text-align:right;
}
.comment-group{
    margin-top: 20px;
    border:1px solid #e8e8e8;
    padding:10px;
}
.add-comment-group{
    margin-top: 20px;
    padding:10px;
    border:1px solid #e8e8e8;
}
.add-comment-group h3{
    margin-bottom: 10px;
}
.comment-btn-group{
    margin-top: 10px;
    text-align:right;
}
.add-comment-group h3{
    margin-top: 10px;
}
.comment-btn-group{
    margin-top: 10px;
    text-align: right;
}
.comment-list-group li{
    overflow: hidden;
    padding:10px 0;
    border-bottom: 1px solid #e8e8e8;
}
.avatar-group{
    float:left;
}
.avatar-group img{
    width: 50px;
    height: 50px;
    border-radius: 50%;
}
.comment-content{
    float:left;
    margin-left: 10px;
}
#5.在front_base.html里面,当用户登陆了,那么就会有一个login-tag标签,所以加上这个隐藏的标签
{% if g.front_user %}
   <span id="login-tag" data-is-login='1' style="display: none;"></span>
#6.在front_pdetail.html里面将帖子id绑定到标签上
<article class="post-content" id="post-content" data-id="{{post.id}}">
#7.在front_pdetail.js里面初始化ueditor,并post评论
$(function(){
    var ue=UE.getEditor("editor",{
        'serverUrl':'/ueditor/upload/',
        'toolbars':[[
            'undo',//撤销
            'redo',
            'bold',
            'italic',
            'source',
            'blockquote',
            'selectall',
            'insertcode',
            'fontfamily',
            'fontsize',
            'simpleupload',
            'emotion'
        ]]
    });
    window.ue=ue;
});
$(function(){
    $("#comment-btn").click(function(event){
        event.preventDefault();
        var loginTag=$("#login-tag").attr("data-is-login");
        if (!loginTag){
            window.location='/signin/';
        }else{
            var content=window.ue.getContent();
            var post_id=$("#post-content").attr("data-id");
            zlajax.post({
                'url':'/acomment/',
                'data':{'content':content,'post_id':post_id},
                'success':function(data){
                    if(data['code']==200){
                        window.location.reload();
                    }else{
                        zlalert.alertInfo(data['message']);
                    }
                }
            });
        }
    });
});

1.17精华帖子过滤

#1.首先在front_index.html里面写精华帖子的链接,并显示精华帖
{% if current_sort==1 %}
                <li class="active"><a href="{{url_for('front.index' ,st=1,bd=current_board)}}">最新</a></li>
              {% else %}
              <li ><a href="{{url_for('front.index' ,st=1,bd=current_board)}}">最新</a></li>
              {% endif %}
              {% if current_sort==2 %}
                <li><a class="active" href="{{url_for('front.index' ,st=2,bd=current_board)}}">精华帖子</a></li>
              {% else %}
                <li><a href="{{url_for('front.index' ,st=2,bd=current_board)}}">精华帖子</a></li>
              {% endif %}
              {% if current_sort==3 %}
                <li><a class="active" href="{{url_for('front.index' ,st=3,bd=current_board)}}">点赞最多</a></li>
              {% else %}
               <li><a  href="{{url_for('front.index' ,st=3,bd=current_board)}}">点赞最多</a></li>
              {% endif %}
              {% if current_sort==4 %}
                <li><a class="active" href="{{url_for('front.index' ,st=4,bd=current_board)}}">评论最多</a></li>
              {% else %}
                <li><a href="{{url_for('front.index' ,st=4,bd=current_board)}}">评论最多</a></li>
              {% endif %}
   {% if post.highlight %}
                        <span class="label label-danger">精华帖</span>
                      {% endif %}
#2.在front/views里面根据参数st然后排序返回
bp=Blueprint('front',__name__)
@bp.route('/')
def index():
    board_id = request.args.get('bd',type=int,default=None)
    sort=request.args.get('st',type=int,default=1)
    banners=BannerModel.query.order_by(BannerModel.priority.desc()).limit(4)
    boards=BoardModel.query.all()
    # posts=PostModel.query.all()
    page=request.args.get(get_page_parameter(),type=int,default=1)
    start=(page-1)*config.PER_PAGE
    end=start+config.PER_PAGE
    posts=None
    total=0
    query_obj=None
    if sort==1:
        query_obj=PostModel.query.order_by(PostModel.create_time.desc())
    elif sort==2:
        query_obj=db.session.query(PostModel).outerjoin(HighlightPostModel).order_by(HighlightPostModel.create_time.desc(),PostModel.create_time.desc())
    elif sort==3:
        query_obj=PostModel.query.order_by(PostModel.create_time.desc())
    elif sort==4:
        query_obj=db.session.query(PostModel).outerjoin(CommentModel).group_by(PostModel.id).order_by(func.count(CommentModel.id).desc(),PostModel.create_time.desc())
    if board_id:
        query_obj=query_obj.filter_by(board_id=board_id)
        posts=query_obj.slice(start,end)
        total=query_obj.count()
    else:
        posts=query_obj.slice(start,end)
        total=query_obj.count()
    pagination=Pagination(bs_version=3,page=page,total=total,outer_window=0,inner_window=1)
    context={"banners": banners,'boards': boards,'posts': posts,'pagination':pagination,'current_board': board_id,'current_sort':sort}
    return render_template("/front/front_index.html",**context)

3.celery异步优化

task>broker>worker>backend
启动redis

#1.pip install celery eventlet
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是小z呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值