【Flask/跟着學習】Flask大型教程項目#07:關注者

跟着學習(新版):https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vii-error-handling
回顧上一章:https://blog.csdn.net/weixin_41263513/article/details/85036999

本章內容

  • 多對多關係
  • 關注者與被關注者
  • 數據庫模型
  • 關注和取消關注
  • 獲取帖子
  • 單元測試
  • 讓上述功能(關注,取消關注)可視化

多對多關係

我們之前說過,數據庫使用關係建立記錄之間的練習,其中,一對多關係是最常用的,它把一個記錄和一組相關的記錄聯繫在了一起,實現這個關係的時候,要在“多”的一側加入一個外鍵,指向“一”這一側連接的記錄。一對多還是非常好理解的,接下來我們將實現多對多的關係。

多對多關係有點複雜。 例如,考慮一個擁有學生和教師的數據庫。 我可以說學生有很多老師,老師有很多學生。 這就像是來自兩端的兩個重疊的一對多關係。對於這種類型的關係,我應該能夠查詢數據庫並獲得教授給定學生的教師列表,以及教師班級中的學生列表。 這在關係數據庫中表示實際上並不重要,因爲無法通過向現有表添加外鍵來完成。

多對多關係的表示需要使用稱爲關聯表的輔助表。如下圖
在這裏插入圖片描述

關注者與被關注者

根據上面的內容,很容能知道關注者與被關注者是多對多關係,因爲用戶可以關注很多用戶,同時,用戶也可以有許多關注者,例如:我是B站的一個up主,我有不多的粉絲,但同時,我也關注着其他優秀的up主。

所以,在我們的這個項目中,數據庫的圖可以如下畫
followers是輔助表
在這裏插入圖片描述

數據庫模型

現在可以開始了,先完成輔助表,非常簡單:
文件:/app/models.py

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聲明爲一個類(class),因爲這是一個不需要外部輸入的一張輔助表
接下來,需要在users表中聲明多對多的關係:
文件:/app/models.py

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表,左邊和右邊
然後讓我們逐個看看db.relationship()調用的所有參數:
“User”:生成兩個User表嘛,非常容易理解
”secondary“:用來指明輔助表
”primaryjoin“:表示將左邊用戶(發起關注的用戶)與輔助表連接的條件,followers.c.follower_id表達式引用輔助表的follower_id列
”secondaryjoin“:表示右邊用戶(被關注的用戶)與輔助表連接的條件
”backref“:定義瞭如何從右邊用戶訪問這個關係,當我們做出一個名爲 followed 的查詢的時候,將會返回所有跟左邊用戶聯繫的右邊的用戶。當我們做出一個名爲 followers 的查詢的時候,將會返回一個所有跟右邊聯繫的左邊的用戶。簡單來說就是打開粉絲列表(followed),可以看到裏面有多少粉絲(關注自己的人),打開關注列表(follow),可以看到裏面有多少我關注的人。
”lazy“:指示此查詢的執行模式,dynamic模式表示直到有特定的請求才會運行查詢,這是對性能有很好的考慮。
不理解沒關係,後面會伴隨着例子詳細說明

關注和取消關注

以下是User模型中添加和刪除關係的更改:
文件:/app/models.py

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方法執行之前,我們都需要is_following來處理這兩個簡單功能的邏輯關係
is_following方法用來檢查兩個用戶之間的關係是否存在,之前我們使用SQLAlchemy查詢對象的filter_by()方法,例如查找給定用戶名的用戶。我在這裏使用的filter()方法類似,但是更低級別,因爲它可以包括任意過濾條件,不像filter_by(),它只能檢查與常量值的相等性。查詢以count()方法終止,該方法返回結果數。此查詢的結果將爲0或1,因此檢查計數爲1或大於0實際上是等效的。

獲取帖子

正常來說,當我關注了一個人的時候,這個人的動態以及發過的貼子都應該出現在我的index頁面中,因此需要一個返回貼子的數據庫查詢,所以:
文件:/app/models.py

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())

join主要是一個合併表的功能,上面看似複雜,但仔細看也不難琢磨出來

接下來要展現出來的不只是你關注的人寫的貼子,你自己也會寫帖子的嘛,所以需要把你和你關注的人的貼子整合在一起,有一個很簡單的方法,就是自己關注自己,自己成爲自己的關注者,不過一般來說,很多網站都是設置自己不能關注自己,因爲這回影響關於followers的統計數據,反正就是不好啦!

所以可以使用“union”運算符將兩個查詢合併爲一個查詢,更新一下文件:
文件:/app/models.py

    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

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()中實現了一點hack,以防止單元測試使用我用於開發的常規數據庫。通過將應用程序配置更改爲sqlite://我在測試期間讓SQLAlchemy使用內存中的SQLite數據庫。 db.create_all()調用將創建所有數據庫表。這是從頭開始創建數據庫的快速方法,可用於測試

嘗試運行一下嘛~
在這裏插入圖片描述

從現在開始,每次對應用程序進行更改時,您都可以重新運行測試以確保正在測試的功能未受到影響。此外,每次嚮應用程序添加另一個功能時,都應爲其編寫單元測試

讓上述功能(關注,取消關注)可視化

讓我們在應用程序中添加兩個新路由以關注和取消關注用戶:
文件:/app/routes.py

@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))

一定一定一定不要忘了 db.session.commit()
有了視圖函數的功能,現在要創建相應的頁面了:
文件:/app/templates/user.html

   ...
        <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 %}
        ...

可以很容易理解的邏輯是:如果用戶正在查看當前未關注的用戶,則會顯示“關注”鏈接。
如果用戶正在查看當前關注的用戶,則會顯示“取消關注”鏈接。

效果圖效果圖
在這裏插入圖片描述
在這裏插入圖片描述

注意,我將進入用戶資料界面是直接通過http://127.0.0.1:5000/user/123進去的,本來應該在index.html中顯示所關注用戶的貼子列表,但現在編寫貼子的功能還未實現,所以等到下一章再來把!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章