一個web應用的誕生(9)--回到用戶

在開始以前,咱們首先根據以前的內容想象一個場景,用戶張三在網上瀏覽,看到了這個輕博客,發現了感興趣的內容,因而想要爲你們分享一下心情,恩?發現須要註冊,好,輸入用戶名,密碼,郵箱,並上傳頭像後,就能夠愉快的和你們進行分享互動了。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很扎眼對不對,下面完成一下權限系統,說是權限系統,其實只有三個角色:

  1. 匿名用戶,即未登陸用戶,只有閱讀權限
  2. 普通用戶,具備發佈文章,評論文章已經關注他人的權限
  3. 管理員,除普通用戶外,還有刪除及修改文章權限

這三個角色,對應到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">發表於&nbsp;{{ 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>

最後,點擊頭像或姓名,還能夠查看做者的資料,這個功能點分爲三種狀況:

  1. 其餘人觀看,會有一個樣式美觀的名片頁
  2. 本身觀看,則會產生名片頁,並能夠修改內容
  3. 管理員觀看,則會產生名片頁並能夠修改內容

咱們先來看其餘人的我的資料頁,首先,須要建立一個視圖:

@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">&times</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()

添加這個方法便可。

相關文章
相關標籤/搜索