一个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")

看到这个,有必要解释一下了:

  1. foreign_keys很明显是表示外键,因为followers和followed都是与Follow表进行关联,为了消除歧义,必须使用foreign指定特定外键。
  2. backref的作用是回引Follow模型,即即可从用户查询Follow模型,也可直接查询Follow所属的用户
  3. 第一个lazy,即lazy=joined,表示直接通过连接查询来加载对象,即通过一条语句查出用户和所有的followed过的用户(假设followed字段),而假设把它设为select的话,则需要对每个followed的用户进行一次查询操作
  4. 第二个lazy,即lazy=dynamic,表示此操作返回的是一个查询对象,而不是结果对象,可以简单理解为一个半成品的sql语句,可以在其上添加查询条件,返回使用条件之后的结果
  5. 这两个lazy的作用都在一对多关系中的一的一侧设定,即第一个在回引,即直接可以通过已关注的对象找到自己,第二个是在本身,即可以直接返回的已关注列表,并可进行筛选操作(followed字段)
  6. 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 %}
        <!--显示用户列表-->
        &nbsp;&nbsp;<a href="#">共有{{user.followers.count()}}人关注</a>
        &nbsp;&nbsp;<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)

这个视图模型没什么好说的,但需要注意两点:

  1. 很容易可以看到,flask支持在路由中多个动态参数
  2. 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')}}
                          &nbsp;&nbsp;
                          {% 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块即可,但是实现方式则不是那么简单,下面理一下步骤:

  1. 登录用户,一直userid
  2. 根据userid,可获取所有已关注用户
  3. 根据已关注用户,查询发布的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">发表于&nbsp;{{ 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>

不知道为啥,格式乱了,凑合看吧,最终实现效果如下:

全部:

已关注:

看上去不错,但是其实这样会有一个问题,具体是什么问题呢,下一章再来解释并解决。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容