在开始之前,我们首先根据之前的内容想象一个场景,用户张三在网上浏览,看到了这个轻博客,发现了感兴趣的内容,于是想要为大家分享一下心情,恩?发现需要注册,好,输入用户名,密码,邮箱,并上传头像后,就可以愉快的和大家进行分享互动了。
这是一个很好的场景,不是么,下面我们就要来实现它,首先来说,存储一张图片有多重方法,服务器本地存储,db 中存储二进制,但是这些都会或多或少的占用服务器的空间,并且,图片的读写还会占用空间宝贵的流量,对于我来说,一个穷 coder,用的服务器是最便宜的一款阿里云,所以空间能省就省,而流量,更是节约到底,毕竟阿里云的流量比空间还要贵。
最节省的方式当然是使用免费的专有空间来存储图片了,幸运的是,确实有这样一种看上去很天方夜谭的方式,那就是使用七牛云,当然了,免费使用七牛云的话,比如不能绑定域名,单 ip 访问频次限制等,但现阶段来说已经是够用了。
使用七牛云的方法看上去和之前没什么区别,第一项当然还是安装:
- pip3.6 install qiniu
然后进行注册:
- from qiniu import Auth
- ...
- qn=Auth(access_key,secret_key)
很简单,其实这里使用的只是一个获取 token,而文件上传的部分使用 js-jdk 来实现, 现在增加一个获取 token 的视图:
- #获取七牛凭证
- @main.route("/qiniuuptoken",methods=["GET","POST"])
- def qiniuuptoken():
- bucket_name="python-nblog"
- key=str(uuid.uuid1())
- token=qn.upload_token(bucket_name,key)
- return jsonify({
- "uptoken":token,
- "key":key
- })
然后修改用户对象,新增 headimg 字段 (存储文件 key):
- class User(UserMixin,db.Model):
- __tablename__="users"
- ...
- headimg=db.column(db.String(50))
- ...
好了,还记得之前实现的功能么,下面要修改 RegisterForm 类,在表单中新增一个上传头像的 file 域,以及一个用于记录图片 key 的隐藏域
- class RegisterForm(Form):
- ...
- headimg=FileField("上传头像")
- headkey=HiddenField("头像上传后生成的key")
- ...
- submit=SubmitField("提交")
修改 register.html 模板,增加 js 文件的引用块:
- { % block scripts %
- } {
- {
- super()
- }
- } < script src = "http://cdn.bootcss.com/plupload/2.1.9/moxie.min.js" > </script>
- <script src="http:/ / cdn.bootcss.com / plupload / 2.1.9 / plupload.min.js "></script>
- <script src="http: //cdn.bootcss.com/plupload/2.1.9/i18n/zh_CN.js"></script>
- < script src = "http://cdn.bootcss.com/qiniu-js/1.0.17.1/qiniu.min.js" > </script>
- <script type="text/javascript " src=" {
- {
- url_for('static', filename = 'js/qiniuupload.js', key = 12)
- }
- }
- "></script>
- {% endblock %}"
引用的 js 文件貌似还不少,可能也看到了,自己使用的就是 qiniuupload.js, 代码如下:
- $(function() {
- var tempurl = "http://on4ag3uf5.bkt.clouddn.com"; //常量 七牛临时域名地址
- var token = {
- key: "",
- uptoken: ""
- }
- //img回写
- if ($("#headkey").val() != "") {
- reSetImg(tempurl)
- }
- var uploader = Qiniu.uploader({
- runtimes: 'html5',
- // 上传模式,依次退化
- browse_button: 'headimg',
- // 上传选择的点选按钮,必需
- uptoken_func: function(file) { // 在需要获取uptoken时,该方法会被调用
- $.getJSON({
- url: "/qiniuuptoken",
- type: "POST",
- async: false,
- success: function(d) {
- token.up = d.uptoken;
- token.key = d.key;
- }
- }) return token.up;
- },
- get_new_uptoken: false,
- // 设置上传文件的时候是否每次都重新获取新的uptoken
- domain: 'python-nblog',
- // bucket域名,下载资源时用到,必需
- //container: 'container', // 上传区域DOM ID,默认是browser_button的父元素
- max_file_size: '5mb',
- // 最大文件体积限制
- flash_swf_url: 'http://cdn.bootcss.com/plupload/3.1.0/Moxie.swf',
- //引入flash,相对路径
- max_retries: 3,
- // 上传失败最大重试次数
- dragdrop: false,
- // 开启可拖曳上传
- //drop_element: 'container', // 拖曳上传区域元素的ID,拖曳文件或文件夹后可触发上传
- chunk_size: '1mb',
- // 分块上传时,每块的体积
- auto_start: true,
- // 选择文件后自动上传,若关闭需要自己绑定事件触发上传
- init: {
- 'FileUploaded': function(up, file, info) {
- setImg(tempurl, $.parseJSON(info).key)
- },
- 'Key': function(up, file) {
- // do something with key here
- return token.key
- }
- }
- });
- });
- function setImg(tempurl, imgKey) {
- var temphtml = "<div class='form-group'><label class='control-label'>头像预览</label>"temphtml += "<div><img src='" + tempurl + "/" + imgKey + "' class='img-thumbnail' style='width:200px;height:200px;'></div>";
- temphtml += "</div>";
- //修改key
- $("#headkey").val(imgKey)
- //增加预览图
- $("#headimg").parent().after(temphtml);
- $("#headimg").hide();
- }
代码不难懂,除了七牛部分,都是基本的 jq 代码,并且七牛的 js-sdk 都有很完善的 demo 和文档
注意,由于使用的为免费用户,所以不能绑定域名,使用的为七牛分配域名。
然后,修改注册视图:
- if form.validate_on_submit():
- ...
- user.headimg=form.headkey.data
- ...
- user.role_id=1 #暂时约定公开用户角色为1
- db.session.add(user)
最后修改 base.html 模板,将注册页的导航加入:
- <ul class="nav navbar-nav navbar-right">
- {% if current_user.is_authenticated %}
- <li>
- <p class="navbar-text">
- <a href="#" class="navbar-link">
- {{current_user.username}}
- </a>
- 您好
- </p>
- </li>
- <li>
- <a href="{{url_for('auth.logout')}}">
- 登出
- </a>
- </li>
- {% else %}
- <li>
- <a href="{{url_for('auth.login')}}">
- 登录
- </a>
- </li>
- <li>
- <a href="{{url_for('auth.register')}}">
- 注册
- </a>
- </li>
- {% endif %}
- </ul>
功能宣告完成。
与这个功能类似的功能是用户资料的功能,即对用户资料的查看和修改,但这个功能需要用户权限来进行支撑,所以先来完成用户权限。
下面让我们回看之前的代码,user.role_id=1 很扎眼对不对,下面完成一下权限系统,说是权限系统,其实只有三个角色:
这三个角色,对应到 db 中需要两条记录,即 User 和 Administrator,下面对角色类进行适当的修改并增加初始化方法
- class Role(db.Model):
- __tablename__="roles"
- id=db.Column(db.Integer,primary_key=True)
- name=db.Column(db.String(50),unique=True)
- users=db.relationship("User",backref='role')
- default=db.Column(db.Boolean)
- @staticmethod
- def init_roles():
- roles={
- "User":('普通用户',True),
- "Administrator":("管理员用户",False)
- }
- for r in roles:
- print(r)
- role=Role.query.filter_by(name=r[0]).first()
- if role is None:
- role=Role()
- role.name=roles[r][0]
- role.default=roles[r][1]
- db.session.add(role)
- db.session.commit()
增加了一个 default 字段,以绝定用户注册时使用此角色,并且增加了初始化方法,新增两个角色,执行初始化脚本:
- python manage.py shell
- >>>Role.init_roles()
为用户定义默认角色:
- class User(UserMixin,db.Model):
- def __init__(self,**kwargs):
- super(User,self).__init__(**kwargs)
- if self.role is None:
- self.role=Role.query.filter_by(default=True).first();
通过 User 类的构造函数,来发现创建 user 类中是否已经定义了角色,如果没有定义则设置为默认角色。
然后继续创建一个匿名用户类:
- class AnonymousUser(AnonymousUserMixin):
- def is_administrator(self):
- return self.role.admin
可以看到,此匿名用户类继承了 Flask_login 的 AnonymousUserMixin 类,并将其设置为匿名用的 current_user 的值,即未登录用户的 current_user,以便程序中使用。
如果某些视图函数只对登录用户或管理员开发,当让可以在视图内判断,但更好的方式则是使用一个自定义的装饰器。
- from functools import wraps
- from flask import abort
- from flask_login import current_user
- def admin_required(f):
- @wraps(f)
- def decorated_function(*args,**kwargs):
- if not current_user.is_administrator():
- abort(403)
- return f(*args,**kwargs)
- return decorated_function
装饰器使用了 functools 包,功能为如果用户不为管理员,则返回 403 错误,下面演示一下如何使用这个装饰器:
- @main.route("/admin",methods=["GET","POST"])
- @admin_required
- def for_admin_only():
- return "您好 管理员"
运行一下,还记得之前注册过的用户么,就使用 zhangji 这个用户好了,登录后直接在 url 中输入 / admin, 显示:
为了方便测试,直接将 db 中 zhangji 这个用户的 role_id 字段修改为管理员 id,刷新页面:
ok, 非常完美,接下来根据权限,完成首页内容:
首先,头像改为实际内容:
- {% for post in posts %}
- <div class="bs-callout
- {% if loop.index % 2 ==0 %}
- bs-callout-d
- {% endif %}
- {% if loop.last %}
- bs-callout-last
- {% endif %}" >
- <div class="row">
- <div class="col-sm-2 col-md-2">
- <!--使用测试域名-->
- <img src="http://on4ag3uf5.bkt.clouddn.com/{{post.author.headimg}}" alt="...">
- </div>
- <div class="col-sm-10 col-md-10">
- <div>
- <p>
- {% if post.body_html%}
- {{post.body_html|safe}}
- {% else %}
- {{post.body}}
- {% endif %}
- </p>
- </div>
- <div>
- <a class="text-left" href="#">李四</a>
- <span class="text-right">发表于 {{ moment( post.createtime).fromNow(refresh=True)}}</span>
- </div>
- </div>
- </div>
- </div>
- {% endfor %}
以及:
- <div class="col-md-4 col-md-4 col-lg-4">
- <!--这里 当没有用户登录的时候 显示热门分享列表 稍后实现-->
- {% if current_user.is_authenticated %}
- <img src="http://on4ag3uf5.bkt.clouddn.com/{{current_user.headimg}}" alt="..."
- class="headimg img-thumbnail">
- <br>
- <br>
- <p class="text-muted">
- 我已经分享
- <span class="text-danger">
- 55
- </span>
- 条心情
- </p>
- <p class="text-muted">
- 我已经关注了
- <span class="text-danger">
- 7
- </span>
- 名好友
- </p>
- <p class="text-muted">
- 我已经被
- <span class="text-danger">
- 8
- </span>
- 名好友关注
- </p>
- {%endif%}
- </div>
关注部分稍后完成。
而如果没有登录,则是不能分享心情的,这时将表单隐藏即可
- <div>
- {% if current_user.is_authenticated %}
- {{ wtf.quick_form(form) }}
- {% endif %}
- </div>
最后,点击头像或姓名,还可以查看作者的资料,这个功能点分为三种情况:
我们先来看其他人的个人资料页,首先,需要创建一个视图:
- @main.route("/user/<username>")
- def user(username):
- user=User.query.filter_by(username=username).first()
- if(user is None):
- abort(404)
- posts = Post.query.filter_by(author_id=user.id)
- return render_template("user.html",user=user,posts=posts)
然后创建模板:
- { % extends "base.html" %
- } { % block main %
- } < div class = "container" > <div class = "row" > <p > <img src = "http://on4ag3uf5.bkt.clouddn.com/{{user.headimg}}"alt = "..."class = "headimg img-thumbnail"style = "width:300px; height: 300px" > </p>
- <p>
- {% if user.nickname%}{{user.nickname}}{%elif user.username %}{{ user.username }}{% endif %}
- </p > { %
- if user.username %
- } < p > 用户名: {
- {
- user.username
- }
- } < /p>
- {% endif %}
- {% if user.username %}
- <p>昵称:{{user.nickname}}</p > { % endif %
- } { %
- if user.email %
- } < p > 联系方式: < a href = "mailto:{{user.email}}" > {
- {
- user.email
- }
- } < /a></p > { % endif %
- } { %
- if user.remark %
- } < p > 自我简介: {
- {
- user.remark
- }
- } < /p>
- {% endif %}
- <p>
- 注册时间:{{moment(user.createtime).format('LL')}}
- 最终登录时间:{{moment(user.lastseen).format('LL')}}
- </p > </div>
- </div > { % endblock %
- }
你可能注意到 createtime 和 lastseen 两个字段,是基于一般的博客网站,新增加的内容:
- class User(UserMixin,db.Model):
- ...
- lastseen=db.Column(db.DateTime,default=datetime.utcnow)
- createtime=db.Column(db.DateTime,default=datetime.utcnow)
- ...
分别在定义了注册时间和最后访问的时间
最后,为头像和作者的位置增加超链接(index.html):
- ...
- <a class="text-left" href="{{url_for('main.user',username=post.author.username)}}">
- <img src="http://on4ag3uf5.bkt.clouddn.com/{{post.author.headimg}}" alt="...">
- </a>
- ...
- <a class="text-left" href="{{url_for('main.user',username=post.author.username)}}">{{post.author.nickname}}</a>
接下来是自己进入和管理员进入,这时候如果还同样在这个页面进行操作,就会显得复杂,所以比较好的办法是如果是本用户或管理员的话,显示一个编辑的超链接,进行一下跳转进行编辑,同时,由于本用户进行编辑的话,只可以编辑有限几个字段,如生日,真实姓名,自我简介等,但是如果是管理员的话,显然会编辑很多自动,如用户名,权限配置等,所以,会创建两个超链接分别对应本用户的表单和管理员的表单(user.html)。
- <p>
- {% if current_user.is_authenticated and current_user.username==user.username
- %}
- <a href="#">
- 修改个人信息
- </a>
- {% endif %} {% if current_user.is_administrator() %}
- <a href="#">
- 修改该用户信息
- </a>
- {% endif %}
- </p>
下面创建修改个人信息表单:
- from flask_wtf import FlaskForm
- from wtforms import FileField,HiddenField,StringField,DateField,RadioField,TextAreaField,SubmitField
- from wtforms.validators import Email
- class EditProfileForm(FlaskForm):
- headimg = FileField("上传头像")
- headkey = HiddenField("头像上传后生成的key")
- nickname = StringField("昵称")
- birthday = DateField("出生日期")
- email = StringField("邮箱地址", validators=[Email()])
- gender = RadioField("性别", choices=[("0", "男"), ("1", "女")], default=0,coerce=int)
- remark = TextAreaField("自我简介")
- submit = SubmitField("提交")
当修改的时候,头像要能够回写,在 qiniuupload.js 文件中的 $(function(){}) 方法中增加如下方法:
- //img回写
- if($("#headkey").val()!=""){
- reSetImg(tempurl)
- }
并且添加 reSetImg 方法:
- function reSetImg(tempurl) {
- var temphtml="<div class='form-group'><label class='control-label'>头像预览</label>"
- temphtml+="<div><img src='"+tempurl+"/"+$("#headkey").val()+"' class='img-thumbnail' style='width:200px;height:200px;'></div>";
- temphtml+="</div>";
- $("#headimg").parent().after(temphtml);
- }
之前的头像还要删除掉:
- function setImg( tempurl,imgKey){
- var temphtml="<div class='form-group'><label class='control-label'>头像预览</label>"
- temphtml+="<div><img src='"+tempurl+"/"+imgKey+"' class='img-thumbnail' style='width:200px;height:200px;'></div>";
- temphtml+="</div>";
- //删除之前的预览图
- if($("#headimg").parent().next().find("img"))
- {
- $("#headimg").parent().next().remove()
- }
- //修改key
- $("#headkey").val(imgKey)
- //增加预览图
- $("#headimg").parent().after(temphtml);
- $("#headimg").hide();
- }
注意这里删除仅仅是删除 html 中的 dom,七牛中的文件并没有删除,毕竟不是专门针对七牛的 blog 所以这个功能不打算实现,各位可以自己来实现此功能。
而 html 模板与注册模板基本一样:
- { % extends "base.html" %
- } { % block content %
- } < !--具体内容-->{ % import "bootstrap/wtf.html"as wtf %
- } < div class = "container" > <div class = "row" > </div>
- <div class="row">
- <div>
- <div class="page-header">
- <h1>修改个人信息</h1 > </div>
- {% for message in get_flashed_messages() %}
- <div class="alert alert-warning">
- <button type="button" class="close" data-dismiss="alter">×</button > {
- {
- message
- }
- } < /div>
- {% endfor %}
- {{ wtf.quick_form(form)}}
- </div > </div>
- </div > { % endblock %
- } { % block scripts %
- } {
- {
- super()
- }
- } < script src = "http://cdn.bootcss.com/plupload/2.1.9/moxie.min.js" > </script>
- <script src="http:/ / cdn.bootcss.com / plupload / 2.1.9 / plupload.min.js "></script>
- <script src="http: //cdn.bootcss.com/plupload/2.1.9/i18n/zh_CN.js"></script>
- < script src = "http://cdn.bootcss.com/qiniu-js/1.0.17.1/qiniu.min.js" > </script>
- <script type="text/javascript " src=" {
- {
- url_for('static', filename = 'js/qiniuupload.js', key = 01)
- }
- }
- "></script>
- {% endblock %}"
简单测试一下,非常完美,限于篇幅就不贴图,下面完成一下管理员对于普通用户的资料修改, 相对于普通用户来说,管理员要能修改的项就要多一些了,下面创建一个用于管理员使用的表单:
- from flask_wtf import FlaskForm
- from wtforms import FileField,HiddenField,StringField,DateField,RadioField,TextAreaField,SubmitField,SelectField
- from wtforms.validators import Email,ValidationError,DataRequired
- from ..models.User import User
- from ..models.Role import Role
- class EditProfileAdminForm(FlaskForm):
- headimg = FileField("上传头像")
- headkey = HiddenField("头像上传后生成的key")
- username=StringField("用户名",validators=[DataRequired()])
- role=SelectField("用户角色",coerce=int)
- nickname = StringField("昵称")
- birthday = DateField("出生日期")
- email = StringField("邮箱地址", validators=[Email()])
- gender = RadioField("性别", choices=[(0, "男"), (1, "女")], default=0,coerce=int)
- remark = TextAreaField("自我简介")
- submit = SubmitField("提交")
- def __init__(self,user,*args,**kwargs):
- super(EditProfileAdminForm,self).__init__(*args,**kwargs)
- self.role.choices=[(role.id,role.name) for role in Role.query.all()]
- self.user=user;
- def validate_username(self,field):
- if(field.data!=self.username and User.query.filter_by(username=field.data).first()):
- raise ValidationError("此用户名已经使用!")
可以看到,就是在普通的修改页进行了一些修改,增加用户名和角色两个字段,并在构造函数中为角色下拉菜单注入值,主语注入的写法:
- [(role.id,role.name) for role in Role.query.all()]
这种表达式的写法是我决定 python 中最帅的写法,虽然复杂的看着有点晕:(,和 java 中的拉姆达一样,其实应该说 java 中的拉姆达和他一样。还需要注意的一个就是自定义验证的写法,这个验证的功能是如果用户名进行了修改,并且与 db 中已有值相同,则会抛出异常,页面会提示此用户名已经使用,你一定想到了,其实注册的时候就应该做此验证的,同时对注册表单进行修改, 这里就不贴代码。
剩下的就非常简单,和本用户编辑几乎相同,甚至使用相同的模板,下面是视图控制器的代码:
- @main.route("/edit-profile/<int:id>",methods=["GET","POST"])
- @admin_required
- @login_required
- def edit_profile_admin(id):
- user=User.query.get_or_404(id);
- form=EditProfileAdminForm(user=user);
- if form.validate_on_submit():
- user.nickname=form.nickname.data
- user.remark=form.remark.data
- user.birthday=form.birthday.data
- user.email=form.email.data
- user.gender=form.gender.data
- user.headimg=form.headkey.data
- user.role=Role.query.get(form.role.data)
- user.username=form.username.data
- db.session.add(user)
- return redirect(url_for("main.user",username=user.username))
- form.nickname.data=user.nickname
- form.remark.data=user.remark
- form.birthday.data=user.birthday
- form.email.data=user.email
- form.gender.data=user.gender
- form.headkey.data=user.headimg
- form.role.data=user.role_id
- form.username.data=user.username
- return render_template("edit_profile.html",form=form,user=user);
注意此时使用 id 进行用户检索,则可以使用 get_or_404 方法,当查询失败直接报 404 错误
ok,这个功能宣告完成,是不是很简单,发现这篇博文写的有点长了,但是最后还有一个地方要思考一下,就是用户的 lastseen 字段,在什么时候更新合适呢,最简单的方式当然是登录的时候进行更新,但这样真的好吗,想象一下,我在登录后如果进行频繁的操作,那么时间势必会不准确,所以最好的方法是在条件允许的情况下每次 request 的时候都进行更新,当然这样也不可避免的会消耗资源,如何取舍由自己来决定,下面这个例子中实现一下这个功能:
首先在用户模型中添加方法:
- class User(UserMixin,db.Model):
- ...
- def visit(self):
- self.lastseen=datetime.utcnow()
- db.session.add(self);
然后在试图控制器中:
- @auth.before_app_request
- def before_request():
- if(current_user.is_authenticated):
- current_user.visit()
添加这个方法即可。
来源: http://www.cnblogs.com/jiangchao226/p/6629441.html