在开始以前,咱们首先根据以前的内容想象一个场景,用户张三在网上浏览,看到了这个轻博客,发现了感兴趣的内容,因而想要为你们分享一下心情,恩?发现须要注册,好,输入用户名,密码,邮箱,并上传头像后,就能够愉快的和你们进行分享互动了。javascript
这是一个很好的场景,不是么,下面咱们就要来实现它,首先来讲,存储一张图片有多重方法,服务器本地存储,db中存储二进制,可是这些都会或多或少的占用服务器的空间,而且,图片的读写还会占用空间宝贵的流量,对于我来讲,一个穷coder,用的服务器是最便宜的一款阿里云,因此空间能省就省,而流量,更是节约到底,毕竟阿里云的流量比空间还要贵。css
最节省的方式固然是使用免费的专有空间来存储图片了,幸运的是,确实有这样一种看上去很天方夜谭的方式,那就是使用七牛云,固然了,无偿使用七牛云的话,好比不能绑定域名,单ip访问频次限制等,但现阶段来讲已是够用了。html
使用七牛云的方法看上去和以前没什么区别,第一项固然仍是安装:html5
pip3.6 install qiniu
而后进行注册:java
from qiniu import Auth ... qn=Auth(access_key,secret_key)
很简单,其实这里使用的只是一个获取token,而文件上传的部分使用js-jdk来实现,如今增长一个获取token的视图:python
#获取七牛凭证 @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 })
使用一个uuid做为云端的文件名,而且将此uuid与用户绑定存入db中做为用户的头像使用shell
而后修改用户对象,新增headimg字段(存储文件key):json
class User(UserMixin,db.Model): __tablename__="users" ... headimg=db.column(db.String(50)) ...
好了,还记得以前实现的功能么,下面要修改RegisterForm类,在表单中新增一个上传头像的file域,以及一个用于记录图片key的隐藏域flask
class RegisterForm(Form): ... headimg=FileField("上传头像") headkey=HiddenField("头像上传后生成的key") ... submit=SubmitField("提交")
修改register.html模板,增长js文件的引用块:bootstrap
{% 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和文档
七牛的使用步骤
1 注册七牛帐户
2 点击新建存储空间如图示:
4 输入存储空间名称,必填,对应sdk中的domain字段
5 点击肯定 便可
注意,因为使用的为免费用户,因此不能绑定域名,使用的为七牛分配域名。
而后,修改注册视图:
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()
添加这个方法便可。