在開始以前,咱們首先根據以前的內容想象一個場景,用戶張三在網上瀏覽,看到了這個輕博客,發現了感興趣的內容,因而想要爲你們分享一下心情,恩?發現須要註冊,好,輸入用戶名,密碼,郵箱,並上傳頭像後,就能夠愉快的和你們進行分享互動了。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()
添加這個方法便可。