chapter12 - 關注者

本章講的是網路中社交關係的實現,也就是關注與被關注。這裡面涉及到一些稍為複雜的資料庫設計,多對多關係與自引用關係。裡面有些知識,如果之前沒有學過資料庫理論,理解起來可能會有些許困難。

12.1 在資料庫中實現多對多關係

在本書程序中的用戶,可以有關注別人和被別人關注的功能。一個用戶可以關注多個用戶,同時一個用戶也可以被多個用戶關注。

首先要實現關注者與被關注者之間的關係:

app/models.py 關注關聯表的模型實現

書中使用了app/models/user.py這串字元,其實錯了。

class Follow(db.Model): __tablename__ = follows follower_id = db.Column(db.Integer, db.ForeignKey(users.id), primary_key=True) followed_id = db.Column(db.Integer, db.ForeignKey(users.id), primary_key=True) timestamp = db.Column(db.DateTime, default=datetime.utcnow)

關聯表通常都不會直接拿來使用的,而是為其它表提供數據關係。這裡需要在用戶表中使用這裡的關係,用兩個一對多關係實現多對多關係:

app/models.py

書中使用了app/models/user.py這串字元,其實錯的,只是修改的位置是app/models.py文件中的user模型而已

class User(UserMixin, db.Model): # 之前的代碼 followed = db.relationship(Follow, foreign_keys=[Follow.follower_id], backref=db.backref(follower, lazy=joined), lazy=dynamic, cascade=all, delete-orphan) followers = db.relationship(Follow, foreign_keys=[Follow.followed_id], backref=db.backref(followed, lazy=joined), lazy=dynamic, cascade=all, delete-orphan)

這裡把db.backref的lazy參數指定為joined,可以實現從聯結查詢中載入相關對象,不需要分別去查詢關注者和被關注者。而把關係中把lazy設置為dynamic,則可以返回一個查詢對象。create參數設置為all, delete-orphan則可以在刪除記錄時把指向該記錄的實體也刪除,避免出現關聯表中記錄不存在,但指向該記錄的數據還存在於其它表中的情況

程序現在要處理這兩個一對多關係以實現多對多關係,因為這個操作經常需要重複執行,所以這裡在資料庫模型中增加了4個方法,實現關注取消關注正在關注被關注這四個功能。

app/models.py 關注關係的輔助方法

class User(db.Model): # 之前的代碼 def follow(self, user): if not self.is_following(user): f = Follow(follower=self, follwed=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(follower_id=user.id).first() is not None

對於以上實現的資料庫功能部分的測試,作者把代碼放在了本書的Github網站中。

12.2 在資料頁中顯示關注者

這裡要實現幾個功能:

  • 在未關注用戶的資料頁,要出現Follow按鈕
  • 在已關注用戶的資料頁,要出現Unfollow按鈕
  • 在用戶資料頁中顯示關注者和被關注者的數量和用戶列表
  • 在關注了自己的用戶的資料頁中,顯示Follows You標誌

app/templates/user.html 在用戶資料頁上添加關注信息

{% if current_user.can(Permission.FOLLOW) and user != current_user %} {% if not current_user.is_following(user) %} <a href="{{ url_for(.follow, username=user.username) }}" class="btn btn-primary">Follow</a> {% else %} <a href="{{ url_for(.unfollow, username=user.username) }}" class="btn btn-default">Unfollow</a> {% endif %}{% endif %}<a href="{{ url_for(.followers, username=user.username) }}">Followers: <span class="badge">{{ user.followers.count() }}</span></a><a href="{{ url_for(.followed_by, username=user.username) }}">Following: <span class="badge">{{ user.followed.count() }}</span></a>{% if current_user.is_authenticated and user != current_user and user.is_following(current_user) %}| <span class="label label-default">Follows you</span>{% endif %}

app/main/views.py 「關注」和「取消關注」路由和視圖函數

@main.route(/follow/<username>)@login_required@permission_required(Permission.FOLLOW)def follow(username): user = User.query.filter_by(username=username).first() if user is None: flash(Invalid user.) return redirect(url_for(.index)) if current_user.is_following(user): flash(You are already following this user.) return redirect(url_for(.user, username=username)) current_user.follow(user) flash(You are now following %s. % username) return redirect(url_for(.user, username=username))@main.route(/unfollow/<username>)@login_required@permission_required(Permission.FOLLOW)def unfollow(username): user = User.query.filter_by(username=username).first() if user is None: flash(Invalid user) return redirect(url_for(.index)) if not current_user.is_following(user): flash(You are not following this user.) return redirect(url_for(.user, username=username)) current_user.unfollow(user) flash(You are not following %s anymore. % username) return redirect(url_for(.user, username=username))

app/main/views.py 查看「關注者」和「被關注者」的路由和視圖函數

@main.route(/followers/<username>)def followers(username): user = User.query.filter_by(username=username).first() if user is None: flash(Invalid user.) return redirect(url_for(.index)) page = request.args.get(page, 1, type=int) pagination = user.followers.paginate(page, per_page=current_app.config[FLASKY_FOLLOWERS_PER_PAGE], error_out=False) follows = [{user: item.follower, timestamp: item.timestamp} for item in pagination.items] return render_template(followers.html, user=user, title="Followers of", endpoint=.followers, pagination=pagination, follows=follows)@main.route(/followed-by/<username>)def followed_by(username): user = User.query.filter_by(username=username).first() if user is None: flash(Invalid user) return redirect(url_for(.index)) page = request.args.get(page, 1, type=int) pagination = user.followed.paginate(page, per_page=current_app.config[FLASKY_FOLLOWERS_PER_PAGE], error_out=False) follows = [{user: item.followed, timestamp: item.itemstamp} for item in pagination.items] return render_template(followers.html, user=user, title="Followed by", endpoint=.followed_by, pagination=pagination, follows=follows)

注意,這裡用到了程序的配置變數FLASKY_FOLLOWERS_PER_PAGE,需要先在配置文件config.py中添加上去。用到的分頁技術和第11章中用到的是一樣的。

app/template/followers.html 關注與被關注所用到的模板

{% extends "base.html" %}{% import "_macros.html" as macros %}{% block title %}Flasky - {{ title }} {{ user.username }}{% endblock %}{% block page_content %}<div class="page-header"> <h1>{{ title }}{{ user.username }}</h1></div><table class="table table-hover followers"> <thead><tr><th>User</th><th>Since</th></tr></thead> {% for follow in follows %} {% if follow.user != user %} <tr> <td> <a href="{{ url_for(.user, username=follow.user.username) }}"><img class="img-rounded" src="{{ follow.user.gravatar(size=32) }}">{{ follow.user.username }}</a> </td> <td>{{ moment(follow.timestamp).format(L) }}</td> </tr> {% endif %} {% endfor %}</table><div class="pagination"> {{ macros.pagination_widget(pagination, endpoint, username=user.username )}}</div>

12.3 使用資料庫聯結查詢所關注用戶的文章

這裡的「聯結」,就是SQL中的聯結查詢,即同時從多個表中查詢出符合條件的記錄。在本例中,即需要從關注表和博文表中,查詢出所關注用戶所發表的 文章。兩者應該是用用戶ID來關聯的,因為在關注表中,有所關注用戶的用戶ID,而在博文表中,也有作者ID —— 兩個表中的用戶ID和作者ID,其實是同個東西。

app/models.py 獲取所關注用戶的文章

class User(UserMixin, db.Model): # 作者在這裡似乎寫漏了UserMixin # 原有代碼 @property def followed_posts(self): return Post.query.join(Follow, Follow.followed_id == Post.author_id) .flter(Follow.follower_id == self.id)

12.4 在首頁顯示所關注用戶的文章

程序在用戶首頁提供了兩上標籤,分別顯示所有博客文章和所關注用戶的文章

app/main/views.py 顯示所有博客文章或只顯示所關注用戶的文章

@app.route(/, methods=[GET, POST])def index(): # 之前的代碼 show_followed = False if current_user.is_authenticated: show_followed = bool(request.cookies.get(show_followed, )) if show_followed: query = current_user.followed.posts else: query = Post.query pagination = query.order_by(Post.timestamp.desc()).paginate(page, per_page=current_app.config[FLASKY_POSTS_PER_PAGE], error_out=False) posts = pagination.items return render_template(index.html, form=form, posts=posts, show_followed=show_followed, pagination=pagination)

程序使用了cookie來判斷所顯示的文章內容——所有文章還是所關注用戶的文章。這裡用到的cookie在兩上新路由中設定:

app/main/views.py 查詢所有文章還是所關注用戶的文章

@main.route(/all)@login_requireddef show_all(): resp = make_response(redirect(url_for(.index))) resp.set_cookie(show_followed, , max_age=30*24*60*60) return resp@main.route(/followed)@login_requireddef show_followed(): resp = make_response(redirect(url_for(.index))) resp.set_cookie(show_followed, 1, max_age=30*24*60*60) return resp

因為cookie只能在響應對象中設置,所以這兩個路由不能依賴於Flask,要使用makeresponse()方法創建。setcookie()函數的兩個參數分別是cookie的名字和值。max_age參數設置cookie的過期時間,單位為秒,如果不指定,則在瀏覽器關閉後過期。

接下來在要首頁增加兩個導航選項卡,分別調用/all和/followed路由,並在會話中設定正確的值。

app/templates/index.html

<div class="post-tabs"> <ul class="nav nav-tabs"> <li{% if not show_followed %} class="active"{% endif %}><a href="{{ url_for(.show_all) }}">All</a></li> {% if current_user.is_authenticated %} <li{% if show_followed %} class="active"{% endif %}><a href="{{ url_for(.show_followed) }}">Followers</a></li> {% endif %} </ul> {% include _posts.html %}</div>

應把以上代碼放在首頁發表文章的表單後面,分頁信息的前面。

此時,還有個小問題,就是用戶在首頁中查看所關注用戶的文章的時候,會發現自己的文章並沒有出現在其中。這是因為用戶不能關注自己。通常來說,用戶是應當能夠查看到自己所發表的文章的,可以在註冊時就把用戶設為自己的關注者。

app/models.py 構建用戶時把用戶設為自己的關注者

class User(UserMixin, db.Model): # 原有代碼 def __init__(self, **kwargs): # 原有代碼 self.follow(self)

最後,還要把資料庫中所有現有的用戶都設置為自己的關注者

app/models.py

class User(UserMixin, db.Model): # 原有代碼 @staticmethod def add_self_follow(): for user in User.query.all(): if not user.is_following(user): user.follow(user) db.session.add(user) db.session.commit()

然後通過shell來執行這個函數,以更新資料庫

(venv) $ python manage.py shell>>> User.add_self_follows()

但是,把用戶設為自己的關注者之後,在資料頁顯示的關注者和被關注者數量都會多了一個。為了正確顯示,應該在這些數字里減去1。這點在app/templates/user.html模板中的數量那裡減1即可:

...{{ user.followers.count() - 1}}{{ user.followed.count() - 1}}...

推薦閱讀:

WSGI 為什麼很重要?
Django寫的博客工具?
python開發後台,該從哪裡開始,學習過程?
Python 寫的爬蟲爬久了就假死怎麼回事?
PHP 比 Python 牛在哪?

TAG:Flask | Python | Python框架 |