• 一个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 %}
     <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 %}
            <!--显示用户列表-->
            &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{
             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')}}
                              &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的码农来说,当然不能复制粘贴了,这时候可以使用宏页面(" emplates_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">发表于&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>
    

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

    全部:

    已关注:

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

  • 相关阅读:
    使用CSS3实现超炫的Loading(加载)动画效果
    三种简洁的经典高效的DIV+CSS制作的Tab导航简析
    Span和Div的区别
    [总结]Jquery api 快速参考
    25个可遇不可求的jQuery插件
    基于单个 div 的 CSS 绘图
    一款基于jQuery的图片场景标注提示弹窗特效
    HTML5手机开发——滚动和惯性缓动
    发布一个高效的JavaScript分析、压缩工具 JavaScript Analyser
    CSS框架BluePrint
  • 原文地址:https://www.cnblogs.com/jiangchao226/p/6671098.html
Copyright © 2020-2023  润新知