下面回到首页中,使用一个账户登录,你肯定已经注意到了这里的内容:

没错,现在都是写死的一些固定信息,其中分享数量很容易就可以获取,只需要修改首页模板:

  1. <p class="text-muted">我已经分享<span class="text-danger">{{ current_user.posts.count() }}</span>条心情</p>

这样就可以显示,但是关注和被关注显然就不是这么简单了,首先要思考一下,一个人可以关注多个用户,而一个用户也可以被多个人关注,所以,这很明显是一个多对多的关系,而同时,无论是关注用户还是被别人关注,显然都是针对的用户表,所以,这是一个典型的单表自关联的多对多关系,而多对多就需要使用关联表进行连接,下面创建一个关联表(models/Follow.py):

  1. from .. import db
  2. from datetime import datetime
  3. class Follow(db.Model):
  4. __tablename__="follows"
  5. follorer_id=db.Column(db.Integer,db.ForeignKey("users.id"),primary_key=True)
  6. follored_id=db.Column(db.Integer,db.ForeignKey("users.id"),primary_key=True)
  7. createtime=db.Column(db.DateTime,default=datetime.utcnow)

然而这时候,SQLAlchemy框架是无法直接使用的,如果要使用这个关联表,需要把它拆解为两个标准的一对多关系(User.py):

  1. #关注我的
  2. followers = db.relationship("Follow",foreign_keys=[Follow.followed_id],backref=db.backref("followed",lazy='joined'),lazy="dynamic",cascade="all,delete-orphan")
  3. #我关注的
  4. 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表添加些与关注有关的辅助方法

  1. #关注用户
  2. def follow(self,user):
  3. if(not self.is_following(user)):
  4. f=Follow(follower=self,followed=user)
  5. db.session.add(f);
  6. #取消关注
  7. def unfollow(self,user):
  8. f=self.followed.filter_by(followed_id=user.id).first()
  9. if f:
  10. db.session.delete(f);
  11. #我是否关注此用户
  12. def is_following(self,user):
  13. return self.followed.filter_by(followed_id=user.id).first() is not None;
  14. #此用户是否关注了我
  15. def is_followed_by(self,user):
  16. return self.followers.filter_by(followed_id=user.id).first() is not None;

更新一下数据库:

  1. python manage.py db migrate -m "新增用户关注功能"
  2. python manage.py db upgrade

现在就可以把首页用户头像下方内容补充完整:

  1. {% if current_user.is_authenticated %}
  2. <img src="http://on4ag3uf5.bkt.clouddn.com/{{current_user.headimg}}" alt="..." class="headimg img-thumbnail">
  3. <br><br>
  4. <p class="text-muted">我已经分享<span class="text-danger">{{ current_user.posts.count() }}</span>条心情</p>
  5. <p class="text-muted">我已经关注了<span class="text-danger">{{ current_user.followed.count() }}</span>名好友</p>
  6. <p class="text-muted">我已经被<span class="text-danger">{{ current_user.followers.count() }}</span>名好友关注</p>
  7. {%endif%}

刷新一下看看效果:

功能正确实现,但是貌似数据有点惨,下面我们来实现关注功能,其实到了现在这一步,关注功能已经非常的简单,一个最简单的实现方式,在用户资料页面新增一个关注按钮,修改用户资料页:

  1. <p>
  2. {% if current_user.is_authenticated and current_user!=user %}
  3. {% if current_user.is_following(user) %}
  4. <button class="btn btn-primary" type="button">
  5. 已关注 <a href="#" class="badge">取消</a>
  6. </button>
  7. {% else %}
  8. <a href="#" type="button" class="btn btn-primary">关注此用户</a>
  9. {% endif %}
  10. {% endif %}
  11. <!--显示用户列表-->
  12. &nbsp;&nbsp;<a href="#">共有{{user.followers.count()}}人关注</a>
  13. &nbsp;&nbsp;<a href="#">共关注{{user.followed.count()}}人</a>
  14. {% if current_user.is_authenticated and current_user!=user %}
  15. {% if current_user.is_followed_by(user) %}
  16. <span class="label label-default">已关注我</span>
  17. {% endif %}
  18. {% endif %}
  19. </p>

可以看到,很多的超链接的href都为#,下面完善这些指向的视图模型,首先是关注:

  1. @main.route("/follow/<int:userid>",methods=["GET","POST"])
  2. @login_required
  3. def follow(userid):
  4. user=User.query.get_or_404(userid)
  5. if(current_user.is_following(user)):
  6. flash("您不能重复关注用户")
  7. return redirect(url_for(".user",username=user.username))
  8. current_user.follow(user)
  9. flash("您已经成功关注用户 %s" % user.username)
  10. return redirect(url_for(".user", username=user.username))

接下来是取消关注,与关注几乎一模一样:

  1. @main.route("/unfollow/<int:userid>",methods=["GET","POST"])
  2. @login_required
  3. def unfollow(userid):
  4. user = User.query.get_or_404(userid)
  5. if (not current_user.is_following(user)):
  6. flash("您没有关注此用户")
  7. return redirect(url_for(".user", username=user.username))
  8. current_user.unfollow(user)
  9. flash("您已经成功取关用户 %s" % user.username)
  10. return redirect(url_for(".user", username=user.username))

然后是两个用户列表,分别是我关注的用户和关注我的用户,这两个列表除了title之外,几乎一摸一样,所以完全可以使用一个视图模型:

  1. @main.route("/<type>/<int:userid>",methods=["GET","POST"])
  2. def follow_list(type,userid):
  3. user = User.query.get_or_404(userid)
  4. follows= user.followers if "follewer" ==type else user.followed
  5. title=("关注%s用户为:"%user.nickname ) if "follewer" ==type else ("%s关注的用户为"%user.nickname)
  6. return render_template("follow_list.html",user=user,title=title,follows=follows)

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

  1. 很容易可以看到,flask支持在路由中多个动态参数
  2. python中不支持三目表达式,但可以使用 a if 条件 else b来实现三目表达式的功能

而视图模板可以简单设置为如下:

  1. {% extends "base.html" %}
  2. {% block title %}
  3. {{title}}
  4. {% endblock %}
  5. {% block main %}
  6. <style type="text/css">
  7. .media-object{
  8. width: 64px;
  9. height:64px;
  10. }
  11. </style>
  12. <div class="container">
  13. <div class="row">
  14. <div>
  15. {% for follow in follows %}
  16. {% if type=="follower" %}
  17. {% set user=follow.follower %}
  18. {% else %}
  19. {% set user=follow.followed %}
  20. {% endif %}
  21. <div class="
  22. {% if loop.index % 2 ==0 %}
  23. bg-warning
  24. {% else %}
  25. bg-info
  26. {% endif %}
  27. " style="padding: 3px;">
  28. <div class="media">
  29. <div class="media-left">
  30. <a href="#">
  31. <img class="media-object" src="http://on4ag3uf5.bkt.clouddn.com/{{user.headimg}}" alt="...">
  32. </a>
  33. </div>
  34. <div class="media-body">
  35. <h4 class="media-heading">{{user.nickname}}</h4>
  36. {{follow.follower.remark[0,50]}}
  37. <div>
  38. 关注时间:{{moment(follow.createtime).format('LL')}}
  39. &nbsp;&nbsp;
  40. {% if type=="follower" and current_user.id==user.id %}
  41. <a href="{{url_for('main.unfollow',userid=user.id)}}" class="badge">取消关注</a>
  42. {% endif %}
  43. </div>
  44. </div>
  45. </div>
  46. </div>
  47. {% endfor %}
  48. </div>
  49. </div>
  50. </div>
  51. {% endblock %}

同样也比较简单,新的内容只有一点:

  1. {% if type=="follower" %}
  2. {% set user=follow.follower %}
  3. {% else %}
  4. {% set user=follow.followed %}
  5. {% endif %}

set这个语句在jinja2中定义一个变量,对于这里来说,如果参数为follower,则user为follow对象的follower属性,反之则为followed属性。

另外,还需要注意一点,若当前登录用户为“我”,而“我”关注了此用户,则可以取消,若对方关注了“我”,则是没有办法取消的,因为“我”是被关注对象。

最终的显示效果如下:

不懂美工的苦:(

最后,想象一下实际应用场景,在我进入这个轻博客,我首先想要看到的,一般来说,都是我关注的内容,而首页,一般都基于一定的算法,比如热点,热度,时间等挖掘出来的内容,对于数据挖掘这块不会涉及,所以首页只是按时间倒叙即可,但是我关注的内容则需要单独提炼出来,并且各个产品都有不同的展现方式,比如墙外的tumblr登陆用户默认进入一个mine页,展示的都是自己关注的内容,而现在这个轻博客的展示方式则相对更简单,在首页增加一个tab块即可,但是实现方式则不是那么简单,下面理一下步骤:

  1. 登录用户,一直userid
  2. 根据userid,可获取所有已关注用户
  3. 根据已关注用户,查询发布的posts

根据这些步骤,如果直接写sql的话,非常简单,我想只要对follow的逻辑理解了,任何一个入行的人都可以很轻松的写出来:

  1. SELECT posts.* FROM posts LEFT JOIN follows ON posts.author_id=follows.followed_id WHERE follows.follower_id=1

但这个用SQLAlchemy实现稍微有些麻烦,因为涉及了一些新的语法:

  1. db.session.query(Post).select_from(Follow).filter_by(follower_id=self.id).join(Post,Follow.followed_id == Post.author_id)

语法不复杂,但与sql语句的书写顺序稍显不同:

  1. db.session.query(Post) \\查询主表为Post
  2. select_from(Follow) \\关联Follow
  3. filter_by(follower_id=self.id) \\与之前普通查询一样,过滤语句,对应where条件
  4. join(Post,Follow.followed_id == Post.author_id) \\两表联结

为了操作方便,将此语句作为方法新增到user模型中:

  1. class User(UserMixin,db.Model):
  2. ...
  3. def followed_posts(user):
  4. 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)

而视图模型则修改为:

  1. @main.route("/",methods=["GET","POST"])
  2. def index():
  3. form=PostForm()
  4. if form.validate_on_submit():
  5. post=Post(body=form.body.data,author_id=current_user.id)
  6. db.session.add(post);
  7. return redirect(url_for(".index")) #跳回首页
  8. posts=Post.query.order_by(Post.createtime.desc()).all() #首页显示已有博文 按时间排序
  9. 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")

  1. {% macro rander_posts(posts,moment) %}
  2. {% for post in posts %}
  3. <div class="bs-callout
  4. {% if loop.index % 2 ==0 %}
  5. bs-callout-d
  6. {% endif %}
  7. {% if loop.last %}
  8. bs-callout-last
  9. {% endif %}" >
  10. <div class="row">
  11. <div class="col-sm-2 col-md-2">
  12. <!--使用测试域名-->
  13. <a class="text-left" href="{{url_for('main.user',username=post.author.username)}}">
  14. <img src="http://on4ag3uf5.bkt.clouddn.com/{{post.author.headimg}}" alt="...">
  15. </a>
  16. </div>
  17. <div class="col-sm-10 col-md-10">
  18. <div>
  19. <p>
  20. {% if post.body_html%}
  21. {{post.body_html|safe}}
  22. {% else %}
  23. {{post.body}}
  24. {% endif %}
  25. </p>
  26. </div>
  27. <div>
  28. <a class="text-left" href="{{url_for('main.user',username=post.author.username)}}">{{post.author.nickname}}</a>
  29. <span class="text-right">发表于&nbsp;{{ moment( post.createtime).fromNow(refresh=True)}}</span>
  30. </div>
  31. </div>
  32. </div>
  33. </div>
  34. {% endfor %}
  35. {%endmacro%}

注意第二个参数,传入的是moment对象

然后index.html模板修改如下:

  1. ...
  2. {% import "_index_post_macros.html" as macros %}
  3. ...
  4. <div class="col-xs-12 col-md-8 col-md-8 col-lg-8">
  5. <div>
  6. {% if current_user.is_authenticated %}
  7. {{ wtf.quick_form(form) }}
  8. {% endif %}
  9. </div>
  10. <br>
  11. <ul class="nav nav-tabs">
  12. <li role="presentation" class="active"><a href="#all">全部</a></li>
  13. {% if current_user.is_authenticated %}
  14. <li role="presentation"><a href="#follow_post">已关注</a></li>
  15. {% endif %}
  16. </ul>
  17. <div class="tab-content">
  18. <!--全部-->
  19. <div id="all" role="tabpanel" class="tab-pane fade in active">
  20. {{macros.rander_posts(posts,moment)}}
  21. </div>
  22. {% if current_user.is_authenticated %}
  23. <!--已关注-->
  24. <div id="follow_post" role="tabpanel" class="tab-pane fade">
  25. {{macros.rander_posts(follow_post,moment)}}
  26. </div>
  27. {% endif %}
  28. </div>
  29. </div>

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

全部:

已关注:

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

一个web应用的诞生(10)--关注好友的更多相关文章

  1. 一个web应用的诞生(1)--初识flask

    基于flask的web应用的诞生 Flask是一个非常优秀的web框架,它最大的特点就是保持一个简单而易于扩展的小核心,其他的都有用户自己掌握,并且方便替换,甚至,你可以在社区看到众多开源的,可直接用 ...

  2. 从零开始,开发一个 Web Office 套件(10):捕获键盘事件,输入文字

    这是一个系列博客,最终目的是要做一个基于 HTML Canvas 的.类似于微软 Office 的 Web Office 套件(包括:文档.表格.幻灯片--等等). 博客园:<从零开始, 开发一 ...

  3. 一个web应用的诞生(8)--博文发布

    这个系统一直号称轻博客,但貌似博客的功能还没有实现,这一章将简单的实现一个博客功能,首先,当然是为数据库创建一个博文表(models\post.py): from .. import db from ...

  4. 一个web应用的诞生(9)--回到用户

    在开始之前,我们首先根据之前的内容想象一个场景,用户张三在网上浏览,看到了这个轻博客,发现了感兴趣的内容,于是想要为大家分享一下心情,恩?发现需要注册,好,输入用户名,密码,邮箱,并上传头像后,就可以 ...

  5. 一个web应用的诞生(11)--列表分页

    上章的结束,若在实际开发过程中,会发现一个问题,那就首页或关注分享,是一下子按时间顺序全部显示出来,这在实际项目中不可能出现的,想想实际中的产品是如何做的? 一般来说,无非是两种,一种是使用页码,来进 ...

  6. 一个web应用的诞生--数据表单

    下面把角色分为两种,普通用户和管理员用户,至少对于普通用户来说,直接修改DB是不可取的,要有用户注册的功能,下面就开始进行用户注册的开发. 用户表 首先要想好用户注册的时候需要提供什么信息:用户名.密 ...

  7. 一个web应用的诞生(6)--用户账户

    之前登录注册的功能都已经完成,但是登录成功回到首页发现还是白茫茫的一片,对的,title一直都写得博客,那么最终目的也是写出一个轻博客来,但是,在发表文章之前是不是要先记录一下登录状态呢? 用户登录 ...

  8. 一个web应用的诞生(5)--数据表单

    下面把角色分为两种,普通用户和管理员用户,至少对于普通用户来说,直接修改DB是不可取的,要有用户注册的功能,下面就开始进行用户注册的开发. 用户表 首先要想好用户注册的时候需要提供什么信息:用户名.密 ...

  9. 一个web应用的诞生(5)

    下面把角色分为两种,普通用户和管理员用户,至少对于普通用户来说,直接修改DB是不可取的,要有用户注册的功能,下面就开始进行用户注册的开发. 用户表 首先要想好用户注册的时候需要提供什么信息:用户名.密 ...

随机推荐

  1. 【译】C++日志(Logging in C++)

    声明:原创翻译,转载请注明出处!http://www.cnblogs.com/mewmicro/p/6432507.html 注:作者Petru Marginean,2007/09/05   日志记录 ...

  2. JAVA对特殊的字符串进行html编码

    SourceURL:about:blank /** * Created by Administrator on 2016/9/22. */public class HtmlEncode { publi ...

  3. Windows Form线程同步

    .Net多线程开发中,经常需要启动工作线程Worker thread处理某些事情,而工作线程中又需要更新主线程UI thread的界面状态.我们只能在主线程中操作界面控件,否则.Net会抛出异常. 那 ...

  4. LAMP部署

    各组件版本:Linux:CentOS 7 1511 64位 (提前下载)Apache:Apache 2Mysql:MariaDB 5.5.52Php 5.4.16 VMware Workstation ...

  5. Node.js~在linux上的部署~外网不能访问node.js网站的解决方法

    这是上一篇node.js部署到linux上的后续文章,当我们安装完node.js之后,建立了sailsjs的网站,然后在外面电脑上无法访问这个网站,这个问题我们如何去解决? 解决思路: 查看linux ...

  6. 数据库笔试面试题库(Oracle、MySQL等)

    数据库笔试面试题库(Oracle.MySQL等) 版权声明:版权所有,欢迎分享本文,转载请保留出处,否则追究法律责任,谢谢合作. 注:本文将持续更新,可关注作者微信公众号以便获得最新笔试面试资料. ⊙ ...

  7. HTML5之多媒体

    概览 html5新增了两个关于多媒体的元素:video和audio,前者是用于视频,后者用于音频.而他们使用非常简单 <audio src="xhn.mp3" control ...

  8. apicloud上传图片

    //定义api $("#photo").change(function(){ //获取图片 api.getPicture({ sourceType: 'library', enco ...

  9. Selenium 切换句柄

    最近用了网络上别人的一段切换窗口的code每次成功了,不错,学习 // 根据Title切换新窗口 public boolean switchToWindow_Title(WebDriver drive ...

  10. 关于极光推送在手机系统低于iOS10的手机上闪退的问题。

    最近项目中用到了极光推送,升级到了最新的SDK 2.1.9版本,发现只能在iOS10 上运行,其他测试的时候真机闪退.贴上一个可能的原因: