下面回到首頁中,使用一個賬戶登錄,你肯定已經注意到了這里的內容:
沒錯,現在都是寫死的一些固定信息,其中分享數量很容易就可以獲取,只需要修改首頁模板:
<p class="text-muted">我已經分享<span class="text-danger">{{ current_user.posts.count() }}</span>條心情</p>
這樣就可以顯示,但是關注和被關注顯然就不是這么簡單了,首先要思考一下,一個人可以關注多個用戶,而一個用戶也可以被多個人關注,所以,這很明顯是一個多對多的關系,而同時,無論是關注用戶還是被別人關注,顯然都是針對的用戶表,所以,這是一個典型的單表自關聯的多對多關系,而多對多就需要使用關聯表進行連接,下面創建一個關聯表(models/Follow.py):
from .. import db
from datetime import datetime
class Follow(db.Model):
__tablename__="follows"
follorer_id=db.Column(db.Integer,db.ForeignKey("users.id"),primary_key=True)
follored_id=db.Column(db.Integer,db.ForeignKey("users.id"),primary_key=True)
createtime=db.Column(db.DateTime,default=datetime.utcnow)
然而這時候,SQLAlchemy框架是無法直接使用的,如果要使用這個關聯表,需要把它拆解為兩個標准的一對多關系(User.py):
#關注我的
followers = db.relationship("Follow",foreign_keys=[Follow.followed_id],backref=db.backref("followed",lazy='joined'),lazy="dynamic",cascade="all,delete-orphan")
#我關注的
followed = db.relationship("Follow", foreign_keys=[Follow.follower_id], backref=db.backref("follower", lazy='joined'), lazy="dynamic",cascade="all,delete-orphan")
看到這個,有必要解釋一下了:
- foreign_keys很明顯是表示外鍵,因為followers和followed都是與Follow表進行關聯,為了消除歧義,必須使用foreign指定特定外鍵。
- backref的作用是回引Follow模型,即即可從用戶查詢Follow模型,也可直接查詢Follow所屬的用戶
- 第一個lazy,即lazy=joined,表示直接通過連接查詢來加載對象,即通過一條語句查出用戶和所有的followed過的用戶(假設followed字段),而假設把它設為select的話,則需要對每個followed的用戶進行一次查詢操作
- 第二個lazy,即lazy=dynamic,表示此操作返回的是一個查詢對象,而不是結果對象,可以簡單理解為一個半成品的sql語句,可以在其上添加查詢條件,返回使用條件之后的結果
- 這兩個lazy的作用都在一對多關系中的一的一側設定,即第一個在回引,即直接可以通過已關注的對象找到自己,第二個是在本身,即可以直接返回的已關注列表,並可進行篩選操作(followed字段)
- cascade表示主表字段發生變化的時候,外鍵關聯表的響應規則,all表示假設新增用戶后,自動更新所有的關系對象,all也為默認值,但在這個關系中,刪除用戶后顯然不能刪除所有與他關聯的用戶,包括他關注的和關注他的,所以使用delete-orphan的刪除選項,即只刪除關聯關系的對象,對於這個例子來說,也就是所有Follow對象
下面在為User表添加些與關注有關的輔助方法
#關注用戶
def follow(self,user):
if(not self.is_following(user)):
f=Follow(follower=self,followed=user)
db.session.add(f);
#取消關注
def unfollow(self,user):
f=self.followed.filter_by(followed_id=user.id).first()
if f:
db.session.delete(f);
#我是否關注此用戶
def is_following(self,user):
return self.followed.filter_by(followed_id=user.id).first() is not None;
#此用戶是否關注了我
def is_followed_by(self,user):
return self.followers.filter_by(followed_id=user.id).first() is not None;
更新一下數據庫:
python manage.py db migrate -m "新增用戶關注功能"
python manage.py db upgrade
現在就可以把首頁用戶頭像下方內容補充完整:
{% 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">{{ current_user.posts.count() }}</span>條心情</p>
<p class="text-muted">我已經關注了<span class="text-danger">{{ current_user.followed.count() }}</span>名好友</p>
<p class="text-muted">我已經被<span class="text-danger">{{ current_user.followers.count() }}</span>名好友關注</p>
{%endif%}
刷新一下看看效果:
功能正確實現,但是貌似數據有點慘,下面我們來實現關注功能,其實到了現在這一步,關注功能已經非常的簡單,一個最簡單的實現方式,在用戶資料頁面新增一個關注按鈕,修改用戶資料頁:
<p>
{% if current_user.is_authenticated and current_user!=user %}
{% if current_user.is_following(user) %}
<button class="btn btn-primary" type="button">
已關注 <a href="#" class="badge">取消</a>
</button>
{% else %}
<a href="#" type="button" class="btn btn-primary">關注此用戶</a>
{% endif %}
{% endif %}
<!--顯示用戶列表-->
<a href="#">共有{{user.followers.count()}}人關注</a>
<a href="#">共關注{{user.followed.count()}}人</a>
{% if current_user.is_authenticated and current_user!=user %}
{% if current_user.is_followed_by(user) %}
<span class="label label-default">已關注我</span>
{% endif %}
{% endif %}
</p>
可以看到,很多的超鏈接的href都為#,下面完善這些指向的視圖模型,首先是關注:
@main.route("/follow/<int:userid>",methods=["GET","POST"])
@login_required
def follow(userid):
user=User.query.get_or_404(userid)
if(current_user.is_following(user)):
flash("您不能重復關注用戶")
return redirect(url_for(".user",username=user.username))
current_user.follow(user)
flash("您已經成功關注用戶 %s" % user.username)
return redirect(url_for(".user", username=user.username))
接下來是取消關注,與關注幾乎一模一樣:
@main.route("/unfollow/<int:userid>",methods=["GET","POST"])
@login_required
def unfollow(userid):
user = User.query.get_or_404(userid)
if (not current_user.is_following(user)):
flash("您沒有關注此用戶")
return redirect(url_for(".user", username=user.username))
current_user.unfollow(user)
flash("您已經成功取關用戶 %s" % user.username)
return redirect(url_for(".user", username=user.username))
然后是兩個用戶列表,分別是我關注的用戶和關注我的用戶,這兩個列表除了title之外,幾乎一摸一樣,所以完全可以使用一個視圖模型:
@main.route("/<type>/<int:userid>",methods=["GET","POST"])
def follow_list(type,userid):
user = User.query.get_or_404(userid)
follows= user.followers if "follewer" ==type else user.followed
title=("關注%s用戶為:"%user.nickname ) if "follewer" ==type else ("%s關注的用戶為"%user.nickname)
return render_template("follow_list.html",user=user,title=title,follows=follows)
這個視圖模型沒什么好說的,但需要注意兩點:
- 很容易可以看到,flask支持在路由中多個動態參數
- python中不支持三目表達式,但可以使用 a if 條件 else b來實現三目表達式的功能
而視圖模板可以簡單設置為如下:
{% extends "base.html" %}
{% block title %}
{{title}}
{% endblock %}
{% block main %}
<style type="text/css">
.media-object{
width: 64px;
height:64px;
}
</style>
<div class="container">
<div class="row">
<div>
{% for follow in follows %}
{% if type=="follower" %}
{% set user=follow.follower %}
{% else %}
{% set user=follow.followed %}
{% endif %}
<div class="
{% if loop.index % 2 ==0 %}
bg-warning
{% else %}
bg-info
{% endif %}
" style="padding: 3px;">
<div class="media">
<div class="media-left">
<a href="#">
<img class="media-object" src="http://on4ag3uf5.bkt.clouddn.com/{{user.headimg}}" alt="...">
</a>
</div>
<div class="media-body">
<h4 class="media-heading">{{user.nickname}}</h4>
{{follow.follower.remark[0,50]}}
<div>
關注時間:{{moment(follow.createtime).format('LL')}}
{% if type=="follower" and current_user.id==user.id %}
<a href="{{url_for('main.unfollow',userid=user.id)}}" class="badge">取消關注</a>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
同樣也比較簡單,新的內容只有一點:
{% if type=="follower" %}
{% set user=follow.follower %}
{% else %}
{% set user=follow.followed %}
{% endif %}
set這個語句在jinja2中定義一個變量,對於這里來說,如果參數為follower,則user為follow對象的follower屬性,反之則為followed屬性。
另外,還需要注意一點,若當前登錄用戶為“我”,而“我”關注了此用戶,則可以取消,若對方關注了“我”,則是沒有辦法取消的,因為“我”是被關注對象。
最終的顯示效果如下:
不懂美工的苦:(
最后,想象一下實際應用場景,在我進入這個輕博客,我首先想要看到的,一般來說,都是我關注的內容,而首頁,一般都基於一定的算法,比如熱點,熱度,時間等挖掘出來的內容,對於數據挖掘這塊不會涉及,所以首頁只是按時間倒敘即可,但是我關注的內容則需要單獨提煉出來,並且各個產品都有不同的展現方式,比如牆外的tumblr登陸用戶默認進入一個mine頁,展示的都是自己關注的內容,而現在這個輕博客的展示方式則相對更簡單,在首頁增加一個tab塊即可,但是實現方式則不是那么簡單,下面理一下步驟:
- 登錄用戶,一直userid
- 根據userid,可獲取所有已關注用戶
- 根據已關注用戶,查詢發布的posts
根據這些步驟,如果直接寫sql的話,非常簡單,我想只要對follow的邏輯理解了,任何一個入行的人都可以很輕松的寫出來:
SELECT posts.* FROM posts LEFT JOIN follows ON posts.author_id=follows.followed_id WHERE follows.follower_id=1
但這個用SQLAlchemy實現稍微有些麻煩,因為涉及了一些新的語法:
db.session.query(Post).select_from(Follow).filter_by(follower_id=self.id).join(Post,Follow.followed_id == Post.author_id)
語法不復雜,但與sql語句的書寫順序稍顯不同:
db.session.query(Post) \\查詢主表為Post
select_from(Follow) \\關聯Follow
filter_by(follower_id=self.id) \\與之前普通查詢一樣,過濾語句,對應where條件
join(Post,Follow.followed_id == Post.author_id) \\兩表聯結
為了操作方便,將此語句作為方法新增到user模型中:
class User(UserMixin,db.Model):
...
def followed_posts(user):
return None if not user.is_administrator() else db.session.query(Post).select_from(Follow).filter_by(follower_id=user.id).join(Post,Follow.followed_id == Post.author_id)
而視圖模型則修改為:
@main.route("/",methods=["GET","POST"])
def index():
form=PostForm()
if form.validate_on_submit():
post=Post(body=form.body.data,author_id=current_user.id)
db.session.add(post);
return redirect(url_for(".index")) #跳回首頁
posts=Post.query.order_by(Post.createtime.desc()).all() #首頁顯示已有博文 按時間排序
return render_template("index.html",form=form,posts=posts,follow_post=User.followed_posts(current_user))
在首頁模板中,全部post和已關注用戶的post除了post的list之外,其余的內容一模一樣,作為一個有bigger的碼農來說,當然不能復制粘貼了,這時候可以使用宏頁面("\templates_index_post_macros.html")
{% macro rander_posts(posts,moment) %}
{% 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">
<!--使用測試域名-->
<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>
</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="{{url_for('main.user',username=post.author.username)}}">{{post.author.nickname}}</a>
<span class="text-right">發表於 {{ moment( post.createtime).fromNow(refresh=True)}}</span>
</div>
</div>
</div>
</div>
{% endfor %}
{%endmacro%}
注意第二個參數,傳入的是moment對象
然后index.html模板修改如下:
...
{% import "_index_post_macros.html" as macros %}
...
<div class="col-xs-12 col-md-8 col-md-8 col-lg-8">
<div>
{% if current_user.is_authenticated %}
{{ wtf.quick_form(form) }}
{% endif %}
</div>
<br>
<ul class="nav nav-tabs">
<li role="presentation" class="active"><a href="#all">全部</a></li>
{% if current_user.is_authenticated %}
<li role="presentation"><a href="#follow_post">已關注</a></li>
{% endif %}
</ul>
<div class="tab-content">
<!--全部-->
<div id="all" role="tabpanel" class="tab-pane fade in active">
{{macros.rander_posts(posts,moment)}}
</div>
{% if current_user.is_authenticated %}
<!--已關注-->
<div id="follow_post" role="tabpanel" class="tab-pane fade">
{{macros.rander_posts(follow_post,moment)}}
</div>
{% endif %}
</div>
</div>
不知道為啥,格式亂了,湊合看吧,最終實現效果如下:
全部:
已關注:
看上去不錯,但是其實這樣會有一個問題,具體是什么問題呢,下一章再來解釋並解決。