下面回到首頁中,使用一個賬戶登錄,你肯定已經注意到了這里的內容:
沒錯,現(xiàn)在都是寫死的一些固定信息,其中分享數(shù)量很容易就可以獲取,只需要修改首頁模板:
<p class="text-muted">我已經分享<span class="text-danger">{{ current_user.posts.count() }}</span>條心情</p>
這樣就可以顯示,但是關注和被關注顯然就不是這么簡單了,首先要思考一下,一個人可以關注多個用戶,而一個用戶也可以被多個人關注,所以,這很明顯是一個多對多的關系,而同時,無論是關注用戶還是被別人關注,顯然都是針對的用戶表,所以,這是一個典型的單表自關聯(lián)的多對多關系,而多對多就需要使用關聯(lián)表進行連接,下面創(chuàng)建一個關聯(lián)表(models/Follow.py):
from .. import dbfrom datetime import datetimeclass 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框架是無法直接使用的,如果要使用這個關聯(lián)表,需要把它拆解為兩個標準的一對多關系(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表進行關聯(lián),為了消除歧義,必須使用foreign指定特定外鍵。
backref的作用是回引Follow模型,即即可從用戶查詢Follow模型,也可直接查詢Follow所屬的用戶
第一個lazy,即lazy=joined,表示直接通過連接查詢來加載對象,即通過一條語句查出用戶和所有的followed過的用戶(假設followed字段),而假設把它設為select的話,則需要對每個followed的用戶進行一次查詢操作
第二個lazy,即lazy=dynamic,表示此操作返回的是一個查詢對象,而不是結果對象,可以簡單理解為一個半成品的sql語句,可以在其上添加查詢條件,返回使用條件之后的結果
這兩個lazy的作用都在一對多關系中的一的一側設定,即第一個在回引,即直接可以通過已關注的對象找到自己,第二個是在本身,即可以直接返回的已關注列表,并可進行篩選操作(followed字段)
cascade表示主表字段發(fā)生變化的時候,外鍵關聯(lián)表的響應規(guī)則,all表示假設新增用戶后,自動更新所有的關系對象,all也為默認值,但在這個關系中,刪除用戶后顯然不能刪除所有與他關聯(lián)的用戶,包括他關注的和關注他的,所以使用delete-orphan的刪除選項,即只刪除關聯(lián)關系的對象,對于這個例子來說,也就是所有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;
更新一下數(shù)據(jù)庫:
python manage.py db migrate -m "新增用戶關注功能"python manage.py db upgrade
現(xiàn)在就可以把首頁用戶頭像下方內容補充完整:
{% 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%}
刷新一下看看效果:
功能正確實現(xiàn),但是貌似數(shù)據(jù)有點慘,下面我們來實現(xiàn)關注功能,其實到了現(xiàn)在這一步,關注功能已經非常的簡單,一個最簡單的實現(xiàn)方式,在用戶資料頁面新增一個關注按鈕,修改用戶資料頁:
<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_requireddef 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_requireddef 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支持在路由中多個動態(tài)參數(shù)
python中不支持三目表達式,但可以使用 a if 條件 else b來實現(xiàn)三目表達式的功能
而視圖模板可以簡單設置為如下:
{% 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中定義一個變量,對于這里來說,如果參數(shù)為follower,則user為follow對象的follower屬性,反之則為followed屬性。
另外,還需要注意一點,若當前登錄用戶為“我”,而“我”關注了此用戶,則可以取消,若對方關注了“我”,則是沒有辦法取消的,因為“我”是被關注對象。
最終的顯示效果如下:
不懂美工的苦:(
最后,想象一下實際應用場景,在我進入這個輕博客,我首先想要看到的,一般來說,都是我關注的內容,而首頁,一般都基于一定的算法,比如熱點,熱度,時間等挖掘出來的內容,對于數(shù)據(jù)挖掘這塊不會涉及,所以首頁只是按時間倒敘即可,但是我關注的內容則需要單獨提煉出來,并且各個產品都有不同的展現(xiàn)方式,比如墻外的tumblr登陸用戶默認進入一個mine頁,展示的都是自己關注的內容,而現(xiàn)在這個輕博客的展示方式則相對更簡單,在首頁增加一個tab塊即可,但是實現(xiàn)方式則不是那么簡單,下面理一下步驟:
登錄用戶,一直userid
根據(jù)userid,可獲取所有已關注用戶
根據(jù)已關注用戶,查詢發(fā)布的posts
根據(jù)這些步驟,如果直接寫sql的話,非常簡單,我想只要對follow的邏輯理解了,任何一個入行的人都可以很輕松的寫出來:
SELECT posts.* FROM posts LEFT JOIN follows ON posts.author_id=follows.followed_id WHERE follows.follower_id=1
但這個用SQLAlchemy實現(xiàn)稍微有些麻煩,因為涉及了一些新的語法:
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) \\查詢主表為Postselect_from(Follow) \\關聯(lián)Followfilter_by(follower_id=self.id) \\與之前普通查詢一樣,過濾語句,對應where條件join(Post,Follow.followed_id == Post.author_id) \\兩表聯(lián)結
為了操作方便,將此語句作為方法新增到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>