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

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

<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{
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')}}
&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)}}">
<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>

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

全部:

已关注:

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

一个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. jsp内置对象的方法

    JSP内置对象的方法:out:out.print();request:request对象主要用于出列客户端请求.   常用方法:    String getParameter(String name) ...

  2. Git学习之路(6)- 分支操作

    ▓▓▓▓▓▓ 大致介绍 几乎所有的版本控制系统都会支持分支操作,分支可以让你在不影响开发主线的情况下,随心所欲的实现你的想法,但是在大多数的版本控制系统中,这个过程的效率是非常低的.就比如我在没有学习 ...

  3. BZOJ 1898: [Zjoi2004]Swamp 沼泽鳄鱼(矩阵乘法)

    可以发现,如果没有鳄鱼,那么就是裸地一道题,但是可以发现鳄鱼最多每12次重复,那么就少于12的那部分dp,其他的就矩阵乘法就行了 PS:第一次吧矩阵乘法AC了好开心QAQ CODE: #include ...

  4. 【openstack N版】——计算服务nova

    一.openstack计算服务nova 1.1nova介绍 Nova是openstack最早的两块模块之一,另一个是对象存储swift.在openstack体系中一个叫做计算节点,一个叫做控制节点.这 ...

  5. 【排序算法】快速插入排序算法 Java实现

    基本思想 每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部插入完成. 设数组为a[0...n-1] 初始时,a[0]自成一个有序区,无序区为a[1...n-1] ...

  6. 第十九篇 js高级知识---词法分析和AO 链

    上面一篇文章说了js的作用域链,这一节算是对上面的延申,有一个典型的例子,首先看原来的一段代码: var name = "test"; function t() { var b = ...

  7. 前端性能监控:window.performance

    window.performance 是W3C性能小组引入的新的API,目前IE9以上的浏览器都支持.一个performance对象的完整结构如下图所示: memory字段代表JavaScript对内 ...

  8. Cloud9vue&vux上传github小步骤

    成功后创建出以下文件,再输入: git init 再输入:$ git remote add origin https://github.com/github用户名/vux1 然后:git add. 按 ...

  9. Alamofire源码解读系列(三)之通知处理(Notification)

    本篇讲解swift中通知的用法 前言 通知作为传递事件和数据的载体,在使用中是不受限制的.由于忘记移除某个通知的监听,会造成很多潜在的问题,这些问题在测试中是很难被发现的.但这不是我们这篇文章探讨的主 ...

  10. Ubuntu14.04: Error found when loading /root/.profile

    问题描述: 启用root账号登录后系统出现如下提示信息: Error found when loading /root/.profile stdin:is not a tty 解决方法: 在终端中用命 ...