小引

社交网往往有相互关注的特性,本节即添加“Followers”特性。
重点是调整 db,使之能够追踪 who is following whom。


Database Relationships Revisited

  • 理想的情况是,对每个 user 都能维护一个 list,里面包括它的两类 users (即 followersfollowed),但 Relational Database 没有这种 list 类型。
  • Database 只储存 users 的表,所以须找到一种 relationship 类型,能够表征(model) Userfollowers / followed 之间的 link

Representing Followers

Relationship

  • Many-to-Many: followers - followed
  • Self-referential: followers & followed belong to the same class User.

Association Table: followers

  • followers 表中,只存储了两类 ForeignKey,都指向 User


Database Model Representation

1、创建辅助表 followers

app / models.py: association table

followers = db.Table('followers',db.Column('follower_id', db.Integer, db.ForeignKey('user.id')),db.Column('followed_id', db.Integer, db.ForeignKey('user.id'))
)

注:由于作为辅助表的 followers 中,除了外键,没有其他数据,所以没有使用 model class 的方式定义,而是之间采用 db.Table() 的方式定义。

Since this is an auxiliary table that has no data other than the foreign keys, I created it without an associated model class.

2、创建 Many-to-Manyrelationship

app / models.py: many-to-many followers relationship

class User(UserMixin, db.Model):# ...followed = db.relationship('User', secondary=followers,primaryjoin=(followers.c.follower_id == id),secondaryjoin=(followers.c.followed_id == id),backref=db.backref('followers', lazy='dynamic'), lazy='dynamic')
  • User: 因为是 self-referential 关系,故关系的另一边也是 ‘User’(relationship 名字用 Class 名,而非 table 名)
  • secondary: configures the association table for this relationship
  • primaryjoin: indicates the condition that links the left side entity (the follower user) with the association table。 其中 followers.c.follower_id 指的是 association table 的 follower_id 列。

  • secondaryjoin: indicates the condition that links the right side entity (the followed user) with the association table.

  • backref: 定义逆向关系(defines how this relationship will be accessed from the right side entity)。从左侧看,其追踪的 users 属于 followed;从被追踪的 users来看,则追踪者属于 followers
  • lazy: sets up the query to not run until specifically requested, 同于 posts 中设置的 dynamic(one-to-many)。

注: SQLAlchemy tables 如果未定义成 models,则符号 “c” 是这类表的一种属性。 对这类表,其所有的字段或列,均视为属性 “c” 的子属性(sub-attributes)。

The “c” is an attribute of SQLAlchemy tables that are not defined as models. For these tables, the table columns are all exposed as sub-attributes of this “c” attribute.

3、迁移数据库并升级

(venv) $ flask db migrate -m "followers"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'followers'Generating /home/miguel/microblog/migrations/versions/ae346256b650_followers.py ... done(venv) $ flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade 37f06a334dbf -> ae346256b650, followers

Adding and Removing “follows”

  • 依赖SQLAlchemy ORM,以及定义的 User 的 followed relationship,则在 user之间(前提是已在db存储)可以像 list 一样实现操作:
user1.followed.append(user2)
user1.followed.remove(user2)
  • 为提高复用性,封装成 Usermethods

app / models.py: add and remove followers

class User(UserMixin, db.Model):#...def follow(self, user):if not self.is_following(user):self.followed.append(user)def unfollow(self, user):if self.is_following(user):self.followed.remove(user)def is_following(self, user):return self.followed.filter(followers.c.followed_id == user.id).count() > 0
  1. 相较于 filter_by(),其中 filter() 更加底层,可以包含任意的 filtering 条件语句;
    格式上,filter() 的条件语句更完整严格,须指明 table1.field1== table2.field2,并为”==”。
  2. filter_by(),can only check for equality to a constant value,并且条件语句更简单。

注:尽可能将app的逻辑从路由函数分离出来,放到 models 或其他辅助classes及模块中,因为这样便于后续的 unit testing。

It is always best to move the application logic away from view functions and into models or other auxiliary classes or modules, because as you will see later in this chapter, that makes unit testing much easier.


Obtaining the Posts from Followed Users

因为最终我们希望在 /index 页展示出 followed 的 users 的 posts。
所以写好 followed & followers 的relationship(即 db 支持已完成设置)后,考虑如何获取 followed 的 users 的 posts。

  • 方法一:最容易想到的方法是,首先利用user.followed.all() 来获取 a list of followed users,然后查询每一个 user 的 posts,最终将获取到的所有 posts 并入(merg)到 单个 list 中并按日期排序。
  • 弊端一:如果某个 user 追踪的人数 n 过大(followed is very large),则须先执行 n 次 单个 followed user 的 posts 查询,之后须将返回的 n 个 list of posts 进行merge 和 sorting。
  • 弊端二:因为最终 Home 页会进行页码编订(pagination),首页只会显示最新的若干条 posts,设置显示更多内容的链接。这种情形下,除非我们先获取所有 posts 并按日期进行排序,否则无法知晓追踪的 users 的最新消息。

    As a secondary problem, consider that the application’s home page will eventually have pagination implemented, so it will not display all the available posts but just the first few, with a link to get more if desired. If I’m going to display posts sorted by their date, how can I know which posts are the most recent of all followed users combined, unless I get all the posts and sort them first? This is actually an awful solution that does not scale well.

  • 方法二:因为 Relational Database 擅长 merging & sorting,所以不将 query 放到 app 中,而是交给 db 来执行。db 通过索引(index),可以更高效地进行 query & sorting。语句如下:

app / models.py: followed posts query

class User(db.Model):#...def followed_posts(self):return Post.query.join(followers, (followers.c.followed_id == Post.user_id)).filter(followers.c.follower_id == self.id).order_by(Post.timestamp.desc())

对 Post 进行 query,结构为 Post.query.join(...).filter(...).order_by(...)。思路为:将 post 表中的 user_id 视为 followers 表中 followed_id 一方,先将所有匹配情况拿到(得到所有被追踪的 users 发布的所有 posts),然后筛选被某个特定 user 追踪的 users 发布的posts,最后按 post 的日期排序。

1、首先,将 post 表与 followers 表进行 join,条件是followers.c.followed_id == Post.user_id(待查询 posts 属于 followed,所以 followed_id;如果待查询 posts 属于 follower,则 follower_id)。

  • 若 followed_id 存在,但 user_id 不存在,则此 user 未发布 post。
  • 若 followed_id 不存在,但 user_id 存在,则此 user 未被 追踪。
  • 若多条 followed_id 匹配,则多人追踪此 user。
  • 若多条 user_id 匹配,则此 user 发布多条 posts。

2、然后,进行 filtering。上步获得的是 followers 表中记录的所有 users 追踪的所有 followed 的 users 的 posts,为得到某个 user 的所有 followed 的users 的 posts, 则筛选条件为followers.c.follower_id == self.id

3、按 Post 的 timestamp 进行 倒序排布,即最新的在最上方。


Combining Own and Followed Posts

app / models.py : followed posts + user’s own posts.

    def followed_posts(self):followed = Post.query.join(followers, (followers.c.followed_id == Post.user_id)).filter(followers.c.follower_id == self.id)own = Post.query.filter_by(user_id=self.id)return followed.union(own).order_by(Post.timestamp.desc())

Unit Testing the User Model

为保证以后改变 app 的其他部分后,某些复杂的功能模块仍能正常工作,最好的方式就是写一组自动的测试 case,以后每次有改动,则 re-run 这组 case,看看是否正常工作。

microblog / tests.py: user model unit tests.

from datetime import datetime, timedelta
import unittest
from app import app, db
from app.models import User, Postclass UserModelCase(unittest.TestCase):def setUp(self):app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'db.create_all()def tearDown(self):db.session.remove()db.drop_all()def test_password_hashing(self):u = User(username='susan')u.set_password('cat')self.assertFalse(u.check_password('dog'))self.assertTrue(u.check_password('cat'))def test_avatar(self):u = User(username='john', email='john@example.com')self.assertEqual(u.avatar(128), ('https://www.gravatar.com/avatar/''d4c74594d841139328695756648b6bd6''?d=identicon&s=128'))def test_follow(self):u1 = User(username='john', email='john@example.com')u2 = User(username='susan', email='susan@example.com')db.session.add(u1)db.session.add(u2)db.session.commit()self.assertEqual(u1.followed.all(), [])self.assertEqual(u1.followers.all(), [])u1.follow(u2)db.session.commit()self.assertTrue(u1.is_following(u2))self.assertEqual(u1.followed.count(), 1)self.assertEqual(u1.followed.first().username, 'susan')self.assertEqual(u2.followers.count(), 1)self.assertEqual(u2.followers.first().username, 'john')u1.unfollow(u2)db.session.commit()self.assertFalse(u1.is_following(u2))self.assertEqual(u1.followed.count(), 0)self.assertEqual(u2.followers.count(), 0)def test_follow_posts(self):# create four usersu1 = User(username='john', email='john@example.com')u2 = User(username='susan', email='susan@example.com')u3 = User(username='mary', email='mary@example.com')u4 = User(username='david', email='david@example.com')db.session.add_all([u1, u2, u3, u4])# create four postsnow = datetime.utcnow()p1 = Post(body="post from john", author=u1,timestamp=now + timedelta(seconds=1))p2 = Post(body="post from susan", author=u2,timestamp=now + timedelta(seconds=4))p3 = Post(body="post from mary", author=u3,timestamp=now + timedelta(seconds=3))p4 = Post(body="post from david", author=u4,timestamp=now + timedelta(seconds=2))db.session.add_all([p1, p2, p3, p4])db.session.commit()# setup the followersu1.follow(u2)  # john follows susanu1.follow(u4)  # john follows davidu2.follow(u3)  # susan follows maryu3.follow(u4)  # mary follows daviddb.session.commit()# check the followed posts of each userf1 = u1.followed_posts().all()f2 = u2.followed_posts().all()f3 = u3.followed_posts().all()f4 = u4.followed_posts().all()self.assertEqual(f1, [p2, p4, p1])self.assertEqual(f2, [p2, p3])self.assertEqual(f3, [p3, p4])self.assertEqual(f4, [p4])if __name__ == '__main__':unittest.main(verbosity=2)
  • 引入unittest,作为 class UserModelCase 的 基类(unittest.TestCase
  • setUp()tearDown() ,是 unit testing framework 的两类特殊方法,分别在每个 test 开始之前/结束之后执行。
  • setUp()里面,app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://',使SQLAlchemy 在测试中使用 in-memory SQLite database.
  • 写了四个测试:test_password_hashing(), test_avatar(), test_follow(), test_follow_posts()
  • db 完成操作后,记得 db.session.commit()
(venv)~/Flask_microblog $ python tests.py
[2018-06-13 10:31:14,328] INFO in __init__: Microblog startup
test_avatar (__main__.UserModelCase) ... ok
test_follow (__main__.UserModelCase) ... ok
test_follow_posts (__main__.UserModelCase) ... ok
test_password_hashing (__main__.UserModelCase) ... ok----------------------------------------------------------------------
Ran 4 tests in 0.245sOK

Integrating Followers with the Application

1、写路由函数

app / routes.py: follow and unfollow routes.

@app.route('/follow/<username>')
@login_required
def follow(username):user = User.query.filter_by(username=username).first()if user is None:flash('User {} not found.'.format(username))return redirect(url_for('index'))if user == current_user:flash('You cannot follow yourself!')return redirect(url_for('user', username=username))current_user.follow(user)db.session.commit()flash('You are following {}!'.format(username))return redirect(url_for('user', username=username))@app.route('/unfollow/<username>')
@login_required
def unfollow(username):user = User.query.filter_by(username=username).first()if user is None:flash('User {} not found.'.format(username))return redirect(url_for('index'))if user == current_user:flash('You cannot unfollow yourself!')return redirect(url_for('user', username=username))current_user.unfollow(user)db.session.commit()flash('You are not following {}.'.format(username))return redirect(url_for('user', username=username))
  • 均为 @login_required
  • 首先判断,是否存在 user = User.query.filter_by(username=username).first()
  • user 不存在,则 flash 提示, 并定向至 ‘index’
  • user 存在,且为 current_user (即当前登录用户,通过 @login.user_loader 导入),则无法执行 follow 或 unfollow,flash 提示后,定向至 ‘user’(即个人界面,url_for('user', username=username)
  • user 存在,且不为 curent_user,则可以执行 follow 或者 unfollow (注:均对 user 执行,而不是 username);执行后,均需 db.session.commit()

2、更新模板 user.html

app / templates / user.html: user profile 页面 添加 follow 、 unfollow 链接

        ...<h1>User: {{ user.username }}</h1>{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}{% if user.last_seen %}<p>Last seen on: {{ user.last_seen }}</p>{% endif %}<p>{{ user.followers.count() }} followers, {{ user.followed.count() }} following.</p>{% if user == current_user %}<p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p>{% elif not current_user.is_following(user) %}<p><a href="{{ url_for('follow', username=user.username) }}">Follow</a></p>{% else %}<p><a href="{{ url_for('unfollow', username=user.username) }}">Unfollow</a></p>{% endif %}...
  • last_seen 下面添加 数量标定

<p>{{ user.followers.count() }} followers, {{ user.followed.count() }} following.</p>

  • 如果 logged-in user 浏览自己的网页,则 仍旧显示“Edit”链接。
  • 如果 logged-in user 浏览尚未 follow 的 user 的 ‘user’ (profile),则显示 “Follow” 链接。
  • 如果 logged-in user 浏览已经 follow 的 user 的 ‘user’ (profile),则显示 “Unfollow” 链接。

示例: 拿两个已注册的用户 Kungreye 和 Susan (注意大小写),来测试 follow 及 unfollow 。

  1. 登录 Kungreye (‘/index’)

  2. 进入 Profile 页面 (‘/user/Kungreye’),可以看到是 o followers, 0 following

  3. 更改 addr bar 为 “~/user/Susan”,则进入 Susan 的 Profile。
    同样显示 o followers, 0 following,但对于current_user (即Kungreye)来讲,此 user (即 Susan)尚未被 followed,所以显示 “Follow” 链接。

  4. 追踪 Susan (点击 Follow 后)后,Susan 显示有 1 follower, 0 following.

  5. 返回 Kungreye (点击导航条 Profile,链接至 current_user (即 logged-in 的 Kungreye)的 Profile 页面)。 可以发现 0 follower, 1 following

    参照 base.html 中 Profile 的源码 <a href="{{ url_for('user', username=current_user.username) }}">Profile</a>

以上。

The Flask Mega-Tutorial 之 Chapter 8: Followers相关推荐

  1. 一个极好的适合新手的详细的Flask教程————《The Flask Mega Tutorial》

    教程地址: 原地址:http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world 中文地址:http:/ ...

  2. 8 Flask mega-tutorial 第8章 关注 Followers

    如需转载请注明出处. win10 64位.Python 3.6.3.Notepad++.Chrome 67.0.3396.99(正式版本)(64 位) 注:作者编写时间2018-01-24,linux ...

  3. flask开发桌面应用程序_使用Microsoft Authenticatio将多个破折号应用程序嵌入Flask中...

    flask开发桌面应用程序 Full disclosure: I am not an expert in developing web applications. I don't even consi ...

  4. Flask Web开发:基于Python的Web应用开发实战

    <Flask Web开发:基于Python的Web应用开发实战> 虽然简单的网站(Flask+Python+SAE)已经上线,但只是入门.开发大型网站,系统地学习一遍还是有必要的. 201 ...

  5. flask web api_我使用Express,Flask和ASP.NET重建了相同的Web API。 这是我发现的。

    flask web api I've been shopping around for a back end framework to support a tabletop game app, and ...

  6. 如何使用Python的Flask和Google App Engine构建网络应用

    by Tristan Ganry 由Tristan Ganry 这是一个小型教程项目,用于为初学者学习Flask,API和Google App Engine. (This is a small tut ...

  7. 计算机类免费电子书共享

    列表最早来自stackoverflow上的一个问题:List of freely available programming books 现在在github上进行维护:free-programming ...

  8. python用途与前景-python能用来干什么

    编者按:Python因为简单全面易用而成为近年来大热的编程语言.但是很多人学习了这门余元的语法和基本功能之后却不知道Python能干什么以及怎么做.Realpython上面的一篇文章于是把Python ...

  9. python干啥用的啊-python干什么用的

    编者按:Python因为简单全面易用而成为近年来大热的编程语言.但是很多人学习了这门余元的语法和基本功能之后却不知道Python能干什么以及怎么做.Realpython上面的一篇文章于是把Python ...

最新文章

  1. 网络编程Socket之UDP
  2. 【线上分享】下一代互联网通讯协议:QUIC
  3. SAP CRM WebClient UI element id的生成逻辑
  4. 为什么睡觉时身体突然抖一下?答案吓到我了!
  5. 简述java规范要注意哪些问题_JAVA学习:JAVA基础面试题(经典)
  6. android studio开源代码,Android Studio Set of source 代码源集
  7. jquery ajax和servlet,浅谈ajax在jquery中的请求和servlet中的响应
  8. 使用javap分析Java的字符串操作 1
  9. php curl模拟织梦登录,PHP 模拟浏览器 CURL 采集阿里巴巴
  10. 项目Alpha冲刺Day3
  11. 【土壤分类】基于matlab GUI多类SVM土壤分类【含Matlab源码 1398期】
  12. buck-boost电路中,输入电压增加后,电感电流曲线变化的推导 // 《精通开关电源设计》P44 图2-3
  13. CE教程:植物大战僵尸(单卡片无CD)
  14. ps 条件动作添加 图层锁定和解锁
  15. Git在dev分支获取master分支最新代码
  16. 从Trie树(字典树)谈到后缀树(10.28修订)
  17. eclipse 打开txt文件乱码
  18. explain用法和结果的含义
  19. java毕业设计汽车售后服务管理系统源码+lw文档+mybatis+系统+mysql数据库+调试
  20. “火星一号”项目计划于2026年实现载人登陆火星

热门文章

  1. Maven原始setting.xml文件备份
  2. java 基础 api,Java基础——常用API
  3. 安卓开机画面_【电视技术】索尼液晶电视开机横线,图像上下跳动技改方案
  4. opencv对相似图片一键找不同。(嘻嘻,找不同小游戏作弊神器)
  5. 设置wsl2桥接模式和设置ip
  6. BCAST是什么意思啊
  7. 天翼云打造国云安全品牌 铸牢企业云上安全防线
  8. 《云计算》shell高级编程-正则表达式
  9. 国内怎么captcha测试_通过简单的数字测试或PHP图像处理使CAPTCHA更友好
  10. 【强化学习】实现Atari游戏的自动化学习(仅供参考)