Python Flask Web框架教程 8 关注者

原文

在本章中,我将进一步研究应用程序的数据库。 我希望该应用程序的用户能够轻松选择他们想要关注的其他用户。 因此,我将扩展数据库,以便可以跟踪谁在关注谁,这比你想象的要难。

本章的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'))
)

这是我上面的图中的关联表的直接转换。 请注意,我并未将此表声明为模型,就像我为usersposts表所做的那样。 由于这是一个除了外键之外没有其他数据的辅助表,因此我创建了没有关联模型类的表。

现在,我可以在用户表中声明多对多关系:

# 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模式将查询设置为直到特定请求才能运行,这也是我设置帖子一对多关系的方式。
  • lazybackref中具有相同名称的参数相似,但是此参数适用于左侧查询而不是右侧查询。

如果这很难理解,请不要担心。稍后我将向你展示如何使用这些查询,然后所有内容将变得更加清晰。

对数据库的更改需要记录在新的数据库迁移中:

(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的关系记录在数据库中,就好像它是一个列表一样。 例如,如果我有两个用户存储在user1user2变量中,则可以使用以下简单语句使第一个关注第二个:

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()方法终止,该方法返回结果数。该查询的结果将为01,因此检查计数等于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(...)

Joins

要了解联接操作的作用,我们来看一个示例。 假设我有一个包含以下内容的User表:

idusername
1john
2susan
3mary
4david

为了简单起见,我没有显示用户模型中的所有字段,只是显示了对该查询重要的字段。

假设followers关联表显示用户john正在关注用户susandavid,用户susan正在关注mary,而用户mary正在关注david。 代表以上数据的数据是这样的:

follower_idfollowed_id
12
14
23
34

最后,posts表包含每个用户的一篇文章:

idtextuser_id
1post from susan2
2post from mary3
3post from david4
4post from john1

该表还省略了一些不在此讨论范围内的字段。

这是我再次为此查询定义的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中有多个记录符合条件,则将重复记录这些条目。 如果对于给定帖子,关注者中没有匹配项,则该帖子记录不属于联接的一部分。

使用上面定义的示例数据,联接操作的结果为:

idtextuser_idfollower_idfollowed_id
1post from susan212
2post from mary323
3post from david414
3post from david434

请注意,在所有情况下,user_idfollowed_id列如何相等,因为这是联接条件。 来自用户john的帖子未出现在联接表中,因为在具有john作为关注用户的关注者中没有条目,换句话说,没有人关注johndavid的帖子出现了两次,因为该用户被两个不同的用户关注。

可能尚不清楚我通过创建此联接可以获得什么,但请继续阅读,因为这只是较大查询的一部分。

Filters

join操作为我提供了所有用户关注的所有帖子的列表,这比我真正想要的数据更多。 我只对列表的一个子集感兴趣,即被某一个用户关注的帖子,因此我需要修剪所有不需要的条目,这可以通过filter()调用来完成。

这是查询的过滤器部分:

filter(followers.c.follower_id == self.id)

由于此查询在User类的方法中,因此self.id表达式引用我感兴趣的用户的用户ID。filter()调用选择联接表中将follower_id列设置为此用户的记录。 用户,换句话说,我只保留以该用户为关注者的条目。

假设我感兴趣的用户是john,其id字段设置为1。这是过滤后联接表的外观:

idtextuser_idfollower_idfollowed_id
1post from susan212
3post from david414

这些正是我想要的帖子!

请记住,查询是在Post类上发出的,因此即使我最终获得了由数据库作为该查询的一部分创建的临时表,结果也将是包含在该临时表中的帖子,而没有多余的连接操作添加的列内容 。

Sorting

该过程的最后一步是对结果进行排序。 查询的部分内容为:

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='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 users
        u1 = 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 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/<username>', 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/<username>', 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/<username>')
@login_required
def user(username):
    # ...
    form = EmptyForm()
    return render_template('user.html', user=user, posts=posts, form=form)

现在,我可以在每个用户的个人资料页面中添加关注或取消关注的表单:

<!-- app/templates/user.html: Follow and unfollow links in user profile page. -->

        ...
        <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>
            <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来访问该用户的个人资料页面。确保在发出关注或取消关注时检查关注和关注者用户计数的变化。

我应该在应用程序的索引页面中显示关注帖子的列表,但是由于用户还不能编写博客帖子,所以我还没有完成所有准备工作。因此,我将延迟此更改,直到该功能到位为止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值