一个web应用的诞生(10)--关注好友
下面回到首页中,使用一个账户登录,你肯定已经注意到了这里的内容:
没错,现在都是写死的一些固定信息,其中分享数量很容易就可以获取,只需要修改首页模板:
<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 %}
![...](http://on4ag3uf5.bkt.clouddn.com/{{current_user.headimg}})
<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="#">
![...](http://on4ag3uf5.bkt.clouddn.com/{{user.headimg}})
</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)}}">
![...](http://on4ag3uf5.bkt.clouddn.com/{{post.author.headimg}})
</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>
不知道为啥,格式乱了,凑合看吧,最终实现效果如下:
全部:
已关注:
看上去不错,但是其实这样会有一个问题,具体是什么问题呢,下一章再来解释并解决。