原文
在本章中,我将进一步研究应用程序的数据库。 我希望该应用程序的用户能够轻松选择他们想要关注的其他用户。 因此,我将扩展数据库,以便可以跟踪谁在关注谁,这比你想象的要难。
本章的GitHub链接是:浏览,zip,差异。
我在上面说过,我想维护每个用户的“已关注(followed)”和“关注者(follower)”用户列表。 不幸的是,关系数据库没有可用于这些列表的列表类型,所有的表都具有记录以及这些记录之间的关系。
该数据库有一个代表用户的表,因此剩下的就是提出可以对“关注者”/“已关注”链接进行建模的正确关系类型。 现在是查看基本数据库关系类型的好时机:
我已经在第4章中使用了一对多关系。这是该关系的图表:
one-to-many picture
通过这种关系链接的两个实体是用户和帖子。我说一个用户有很多帖子,而一个帖子有一个用户(或作者)。该关系在数据库中使用“许多”侧上的外键(foreign key)表示。在上述关系中,外键是添加到posts
表中的user_id
字段。该字段将每个帖子链接到用户表中其作者的记录。
很明显,user_id
字段可以直接访问给定帖子的作者,但是相反的方向呢?为了使这种关系有用,我应该能够获取给定用户撰写的帖子列表。 posts
表中的user_id
字段也足以回答此问题,因为数据库具有允许进行有效查询的索引,例如我们“检索所有user_id
为X的帖子”。
多对多关系有点复杂。例如,考虑一个有学生和老师的数据库。我可以说一个学生有很多老师,一个老师有很多学生。就像两端有两个重叠的一对多关系。
对于这种类型的关系,我应该能够查询数据库并获得教授给定学生的教师列表以及教师班级中的学生列表。在关系数据库中表示,这实际上是很简单的,因为不能通过将外键添加到现有表中来完成。
多对多关系的表示需要使用称为关联表的辅助表。这是数据库查找学生和教师示例的方式:
many-to-many picture
虽然乍一看似乎并不明显,但是具有两个外键的关联表能够有效地回答有关该关系的所有查询。
多对一类似于一对多关系。区别在于,这种关系是从“许多”方面看的。
一对一关系是一对多的特例。表示是相似的,但是一个约束被添加到数据库中以防止“许多”侧具有多个链接。在某些情况下,这种类型的关系很有用,但并不像其他类型的关系那样普遍。
查看所有关系类型的摘要,很容易确定跟踪关注者的正确数据模型是多对多关系,因为用户关注许多用户,并且用户具有许多关注者。但是有一个转折。在学生和老师的例子中,我有两个通过多对多关系关联的实体。但是对于关注者,我的用户关注其他用户,因此只有用户。那么,多对多关系的第二个实体是什么?
关系的第二个实体也是用户。一个类的实例链接到同一类的其他实例的关系称为自引用关系,这正是我在这里所拥有的。
这是跟踪自我关注者的自指多对多关系图:
followers picture
followers
表是关系的关联表。 该表中的外键都指向用户表中的条目,因为它会将用户链接到用户。 该表中的每个记录代表关注者用户与关注用户之间的一个链接。 像学生和教师的例子一样,这样的设置允许数据库回答我将永远需要的有关关注者和关注者用户的所有问题。 很简约。
让我们先将关注者添加到数据库中。 这是followers
关联表:
# app/models.py: Followers 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'))
)
这是我上面的图中的关联表的直接转换。 请注意,我并未将此表声明为模型,就像我为users
和posts
表所做的那样。 由于这是一个除了外键之外没有其他数据的辅助表,因此我创建了没有关联模型类的表。
现在,我可以在用户表中声明多对多关系:
# 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')
关系的建立是不简单的。就像我对posts
表的一对多关系所做的那样,我正在使用db.relationship
函数在模型类中定义关系。此关系将User
实例链接到其他User
实例,因此,按照惯例,对于通过该关系链接的一对用户,左侧用户关注右侧用户。我定义的关系是从左边的用户看到的followed
情况,因为当我从左边查询此关系时,我会得到已关注的用户的列表(即右边的用户)。让我们一一检查db.relationship()
调用的所有参数:
User
是关系的右侧实体(左侧实体是父类)。由于这是一种自指代关系,因此我必须在双方上使用相同的类。secondary
配置用于此关系的关联表,我在该类的上方定义了该表。primaryjoin
指示将左侧实体(关注者用户)与关联表链接的条件。关系左侧的加入条件是与关联表的follower_id
字段匹配的用户ID。 followers.c.follower_id
表达式引用关联表的follower_id
列。secondaryjoin
指示将右侧实体(被关注的用户)与关联表链接的条件。此条件类似于primaryjoin
的条件,唯一的区别是,我现在使用的是followed_id
,这是关联表中的另一个外键。backref
定义了如何从右侧实体访问此关系。从左侧开始,关系被命名为followed
,因此从右侧开始,我将使用名称followers
来代表链接到右侧目标用户的所有左侧用户。附加的lazy
参数指示此查询的执行模式。dynamic
模式将查询设置为直到特定请求才能运行,这也是我设置帖子一对多关系的方式。lazy
与backref
中具有相同名称的参数相似,但是此参数适用于左侧查询而不是右侧查询。如果这很难理解,请不要担心。稍后我将向你展示如何使用这些查询,然后所有内容将变得更加清晰。
对数据库的更改需要记录在新的数据库迁移中:
(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
多亏了SQLAlchemy ORM,可以将一个用户关注另一个用户的情况通过followed
的关系记录在数据库中,就好像它是一个列表一样。 例如,如果我有两个用户存储在user1
和user2
变量中,则可以使用以下简单语句使第一个关注第二个:
user1.followed.append(user2)
要取消关注用户,我可以这样做:
user1.followed.remove(user2)
尽管添加和删除关注者非常容易,但是我想在代码中提高可重用性,因此我不会在代码中遍布“ append”和“ remove”。 相反,我将实现“follow”和“unfollow”函数作为用户模型中的方法。 始终尽力将应用程序逻辑从视图函数转移到模型或其他辅助类或模块中,因为如本章后面所述,这将使单元测试变得更加容易。
以下是用户模型中添加和删除关系的更改:
# 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
正如我上面显示的,follow()
和unfollow()
方法使用关系对象的append()
和remove()
方法,但在接触关系之前,它们使用is_following()
支持方法来确保所请求的动作感觉。例如,如果我要求user1
关注user2
,但事实证明该关注关系已经存在于数据库中,则我不想添加重复项。可以将相同的逻辑应用于取消关注。
is_following()
方法对followed
的关系进行查询,以检查两个用户之间的链接是否已存在。你已经看到我之前使用SQLAlchemy查询对象的filter_by()
方法,例如查找给定用户名的用户。我在这里使用的filter()
方法类似,但是级别较低,因为它可以包含任意过滤条件,不像filter_by()
只能检查是否等于常数。我在is_following()
中使用的条件是在关联表中查找将左侧外键设置为self
用户,将右侧外键设置为user
变量的条目。该查询以count()
方法终止,该方法返回结果数。该查询的结果将为0
或1
,因此检查计数等于1
还是大于0
实际上是等效的。你过去看到我使用过的其他查询终止符是all()
和first()
。
数据库中对关注者的支持几乎已经完成,但是还缺少一项重要功能。在应用程序的索引页面中,我将显示由登录用户关注的所有人员撰写的博客文章,因此我需要提出一个数据库查询来返回这些文章。
最明显的解决方案是运行一个查询,该查询返回已关注用户的列表,如你所知,该列表将为user.followed.all()
。然后,对于每个这些返回的用户,我都可以运行查询以获取帖子。一旦获得所有帖子,我便可以将它们合并为一个列表,并按日期对其进行排序。听起来不错?好吧,不是真的。
这种方法有两个问题。如果用户关注一千人该怎么办?我将需要执行一千个数据库查询才能收集所有帖子。然后,我将需要合并并排序内存中的数千个列表。作为第二个问题,考虑到应用程序的主页最终将实现分页,因此它不会显示所有可用的帖子,而仅显示前几个帖子,并且如果需要,可以显示一个链接。如果我要显示按日期排序的帖子,我如何才能知道哪些帖子是所有关注用户中最新的,除非我得到所有帖子并先对其进行排序?这实际上是一个糟糕的解决方案,无法很好地扩展。
确实没有办法避免对博客文章进行合并和排序,但是在应用程序中进行合并会导致效率非常低下。关系数据库擅长这种工作。数据库具有索引,使其可以以更高效的方式执行查询和排序,而我可能可以从自己的角度来执行。因此,我真正想要的是提出一个数据库查询,该查询定义了我想要获取的信息,然后让数据库找出如何以最有效的方式提取该信息。
在下面你可以看到此查询:
# app/models.py: Followed posts query
class User(UserMixin, 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())
这是迄今为止我在此应用程序上使用过的最复杂的查询。 我将尝试一次解密此查询的一个点。 如果查看此查询的结构,你将注意到,由SQLAlchemy查询对象的join()
,filter()
和order_by()
方法设计了三个主要部分:
Post.query.join(...).filter(...).order_by(...)
要了解联接操作的作用,我们来看一个示例。 假设我有一个包含以下内容的User
表:
id | username |
---|---|
1 | john |
2 | susan |
3 | mary |
4 | david |
为了简单起见,我没有显示用户模型中的所有字段,只是显示了对该查询重要的字段。
假设followers
关联表显示用户john
正在关注用户susan
和david
,用户susan
正在关注mary
,而用户mary
正在关注david
。 代表以上数据的数据是这样的:
follower_id | followed_id |
---|---|
1 | 2 |
1 | 4 |
2 | 3 |
3 | 4 |
最后,posts
表包含每个用户的一篇文章:
id | text | user_id |
---|---|---|
1 | post from susan | 2 |
2 | post from mary | 3 |
3 | post from david | 4 |
4 | post from john | 1 |
该表还省略了一些不在此讨论范围内的字段。
这是我再次为此查询定义的join()
调用:
Post.query.join(followers, (followers.c.followed_id == Post.user_id))
我正在调用posts表上的join操作。 第一个参数是关注者关联表,第二个参数是联接条件。 我在此调用中所说的是,我希望数据库创建一个临时表,该表将post和followers表中的数据组合在一起。 数据将根据我作为参数传递的条件进行合并。
我使用的条件是,followers表的followed_id
字段必须等于posts表的user_id
。 为了执行此合并,数据库将从posts表(连接的左侧)获取每个记录,并追加来自followers表(连接的右侧)中符合条件的所有记录。 如果followers中有多个记录符合条件,则将重复记录这些条目。 如果对于给定帖子,关注者中没有匹配项,则该帖子记录不属于联接的一部分。
使用上面定义的示例数据,联接操作的结果为:
id | text | user_id | follower_id | followed_id |
---|---|---|---|---|
1 | post from susan | 2 | 1 | 2 |
2 | post from mary | 3 | 2 | 3 |
3 | post from david | 4 | 1 | 4 |
3 | post from david | 4 | 3 | 4 |
请注意,在所有情况下,user_id
和followed_id
列如何相等,因为这是联接条件。 来自用户john
的帖子未出现在联接表中,因为在具有john
作为关注用户的关注者中没有条目,换句话说,没有人关注john
。 david
的帖子出现了两次,因为该用户被两个不同的用户关注。
可能尚不清楚我通过创建此联接可以获得什么,但请继续阅读,因为这只是较大查询的一部分。
join操作为我提供了所有用户关注的所有帖子的列表,这比我真正想要的数据更多。 我只对列表的一个子集感兴趣,即被某一个用户关注的帖子,因此我需要修剪所有不需要的条目,这可以通过filter()调用来完成。
这是查询的过滤器部分:
filter(followers.c.follower_id == self.id)
由于此查询在User
类的方法中,因此self.id
表达式引用我感兴趣的用户的用户ID。filter()
调用选择联接表中将follower_id
列设置为此用户的记录。 用户,换句话说,我只保留以该用户为关注者的条目。
假设我感兴趣的用户是john
,其id
字段设置为1
。这是过滤后联接表的外观:
id | text | user_id | follower_id | followed_id |
---|---|---|---|---|
1 | post from susan | 2 | 1 | 2 |
3 | post from david | 4 | 1 | 4 |
这些正是我想要的帖子!
请记住,查询是在Post
类上发出的,因此即使我最终获得了由数据库作为该查询的一部分创建的临时表,结果也将是包含在该临时表中的帖子,而没有多余的连接操作添加的列内容 。
该过程的最后一步是对结果进行排序。 查询的部分内容为:
order_by(Post.timestamp.desc())
在这里,我要说的是,结果要按帖子的时间戳字段按降序排序。 按照这种顺序,第一个结果将是最新的博客文章。
我在followed_posts()
函数中使用的查询非常有用,但有一个局限性。人们希望在自己关注的用户的时间表中看到他们自己的帖子,而查询本身不具备此功能。
有两种可能的方式来扩展此查询以包括用户自己的帖子。最直接的方法是保留查询不变,但要确保所有用户都在关注自己。如果你是自己的关注者,则上面显示的查询将找到你自己的帖子以及所有关注者的帖子。这种方法的缺点是会影响有关关注者的统计信息。所有关注者的人数将增加1,因此必须在显示之前对其进行调整。第二种方法是通过创建第二个查询来返回用户自己的帖子,然后使用“联合”运算符将两个查询合并为一个查询。
考虑了这两种选择之后,我决定选择第二种。在展开后,你可以在下面看到followed_posts()
函数,以通过联合包含用户的帖子:
# app/models.py: Followed posts query with 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())
请注意,在应用排序之前,将关注的查询和自己的查询合并为一个查询。
尽管我不考虑关注者实现,但我构建了“复杂”功能,但我认为这也不是小事。 在编写非平凡的代码时,我关心的是确保在我对应用程序的不同部分进行修改时,该代码将来能够继续工作。 确保你已编写的代码在将来继续工作的最佳方法是创建一套自动化测试,你可以在每次进行更改时重新运行它们。
Python包含一个非常有用的单元测试包unittest
,使编写和执行单元测试变得容易。 让我们在tests.py
模块中为User
类中的现有方法编写一些单元测试:
# tests.py: User model unit tests.
from datetime import datetime, timedelta
import unittest
from app import app, db
from app.models import User, Post
class 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='[email protected]')
self.assertEqual(u.avatar(128), ('https://www.gravatar.com/avatar/'
'd4c74594d841139328695756648b6bd6'
'?d=identicon&s=128'))
def test_follow(self):
u1 = User(username='john', email='[email protected]')
u2 = User(username='susan', email='[email protected]')
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 users
u1 = User(username='john', email='[email protected]')
u2 = User(username='susan', email='[email protected]')
u3 = User(username='mary', email='[email protected]')
u4 = User(username='david', email='[email protected]')
db.session.add_all([u1, u2, u3, u4])
# create four posts
now = 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 followers
u1.follow(u2) # john follows susan
u1.follow(u4) # john follows david
u2.follow(u3) # susan follows mary
u3.follow(u4) # mary follows david
db.session.commit()
# check the followed posts of each user
f1 = 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)
我添加了四个测试,这些测试在用户模型中行使密码哈希,用户头像和关注者功能。 setUp()
和tearDown()
方法是单元测试框架分别在每个测试之前和之后执行的特殊方法。 我在setUp()
中用了一点技巧,以防止单元测试使用我用于开发的常规数据库。 通过将应用程序配置更改为sqlite://
,我得到了SQLAlchemy在测试过程中使用内存中的SQLite数据库。 db.create_all()
调用创建所有数据库表。 这是从头开始创建数据库的快速方法,对于测试非常有用。 对于开发和生产用途,我已经向你展示了如何通过数据库迁移来创建数据库表。
你可以使用以下命令运行整个测试套件:
(venv) $ python tests.py
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.494s
OK
从现在开始,每次对应用程序进行更改时,你都可以重新运行测试以确保所测试的功能没有受到影响。另外,每次向应用程序添加其他功能时,都应为其编写单元测试。
现在已经完成了对数据库和模型中的关注者的支持,但是我没有将任何此功能集成到应用程序中,因此现在将要添加。
由于关注和取消关注操作会在应用程序中引入更改,因此我将它们实现为POST
请求,这些请求是由于提交Web表单而从Web浏览器触发的。将这些路由实现为GET
请求会更容易,但随后它们可能会在CSRF攻击(Cross-site request forgery)中被利用(延伸阅读)。由于GET
请求更难防范CSRF,因此应仅将它们用于不引起状态更改的操作。由于提交表单而实现这些方法更好,因为可以将CSRF令牌添加到表单中。
但是,当用户唯一需要做的就是单击“关注”或“取消关注”而不提交任何数据时,如何从Web表单触发关注或取消关注动作?为了使此工作正常,该表单将没有任何数据字段。表单中唯一的元素将是CSRF令牌,该令牌已实现为隐藏字段并由Flast-WTF自动添加,还有一个提交按钮,这将是用户需要单击以触发操作的内容。由于这两个动作几乎相同,因此我将对两个动作使用相同的形式。我将这种表单称为EmptyForm
。
# app/forms.py: Empty form for following and unfollowing.
class EmptyForm(FlaskForm):
submit = SubmitField('Submit')
让我们在应用程序中添加两条新路由来关注和取消关注用户:
# app/routes.py: Follow and unfollow routes.
from app.forms import EmptyForm
# ...
@app.route('/follow/' , methods=['POST'])
@login_required
def follow(username):
form = EmptyForm()
if form.validate_on_submit():
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))
else:
return redirect(url_for('index'))
@app.route('/unfollow/' , methods=['POST'])
@login_required
def unfollow(username):
form = EmptyForm()
if form.validate_on_submit():
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))
else:
return redirect(url_for('index'))
这些路线中的表单处理更加简单,因为我们只需要实现提交部分。 与其他表单(例如,登录和编辑个人资料表单)不同,这两个表单没有自己的页面,这些表单将由user()
路由呈现,并出现在用户的个人资料页面中。 validate_on_submit()
调用失败的唯一原因是CSRF令牌丢失或无效,因此在这种情况下,我只是将应用程序重定向回主页。
如果通过表单验证,则在实际执行关注或取消关注操作之前,我会进行一些错误检查。 这是为了防止意外的问题,并在出现问题时尝试向用户提供有用的消息。
要呈现“关注”或“取消关注”按钮,我需要实例化EmptyForm
对象并将其传递给user.html
模板。 因为这两个动作是互斥的,所以我可以将此通用形式的单个实例传递给模板:
# app/routes.py: Follow and unfollow routes.
@app.route('/user/' )
@login_required
def user(username):
# ...
form = EmptyForm()
return render_template('user.html', user=user, posts=posts, form=form)
现在,我可以在每个用户的个人资料页面中添加关注或取消关注的表单:
...
<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 profilea>p>
{% elif not current_user.is_following(user) %}
<p>
<form action="{{ url_for('follow', username=user.username) }}" method="post">
{{ form.hidden_tag() }}
{{ form.submit(value='Follow') }}
form>
p>
{% else %}
<p>
<form action="{{ url_for('unfollow', username=user.username) }}" method="post">
{{ form.hidden_tag() }}
{{ form.submit(value='Unfollow') }}
form>
p>
{% endif %}
...
对用户个人资料模板的更改在最后一次显示的时间戳记下方添加了一行,该行显示了该用户拥有多少关注者和关注的用户。现在,当你查看自己的个人资料时具有“编辑”链接的行现在可以具有三个可能的链接之一:
为了将EmptyForm()
实例重用于关注表单和取消关注表单,我在呈现提交按钮时传递了一个value
参数。在提交按钮中,value
属性定义了标签,因此,通过此技巧,我可以根据需要向用户呈现的操作来更改提交按钮中的文本。
此时,你可以运行该应用程序,创建一些用户,并与关注和不关注的用户一起玩。唯一需要记住的是键入要关注或取消关注的用户的个人资料页面URL,因为当前无法查看用户列表。例如,如果要关注用户名为susan
的用户,则需要在浏览器的地址栏中键入http://localhost:5000/user/susan
来访问该用户的个人资料页面。确保在发出关注或取消关注时检查关注和关注者用户计数的变化。
我应该在应用程序的索引页面中显示关注帖子的列表,但是由于用户还不能编写博客帖子,所以我还没有完成所有准备工作。因此,我将延迟此更改,直到该功能到位为止。