來說明這本由Miguel Grinberg著作,賴屹民翻譯的《Flask網頁開發 第二版》(Flask Web Development, 2nd Edition),本書的撰寫方式如同網頁開發人員新增應用程式功能,每新增一項功能,就會有新的章節說明這樣的新功能,是使用什麼樣的技術製作而成;由於網頁應用程式具結構性,程式碼之間的撰寫與修改是息息相關的,故作者有提供Repository供讀者檢視與試驗。
我們可以依照本書各章節的順序,逐一檢視各章節在Repository對應的 Branch,藉由各個Branch的切換與推移,可以學習、理解與推演作者修改程式的邏輯,這對於Flask的學習是非常重要的。
「Flask網頁開發」系列網誌,整理的方向也只是羅列修改方向一致的各個Branch,統整為較容易查找的功能主題,並輔以註解說明特殊模組、套件的API如何使用,以及使用的目的,對於完全沒有網頁應用程式開發經驗的讀者來說,依舊是極度難以理解的一系列筆記,建議還是得先學習好Flask的基礎,再掌握好本書作者訂定的目錄框架,最後才能掌握住本書的程式開發方式。
第二部分、範例:社群部落格app
第十一章、部落格文章-部落格文章和分頁(11a、11b、11c、11d)
l 部落格文章和分頁:
git checkout 11a
git checkout 11b
git checkout 11c
git checkout 11d
l 將長串的部落格文章清單分頁,先建立偽造的部落格文章資料,使用的套件為faker:
pip install faker
l app/fake.py:
# 產生偽造的使用者與部落格文章
from random import randint
from sqlalchemy.exc import IntegrityError
from faker import Faker
from . import db
from .models import User, Post
def users(count=100):
fake = Faker()
i = 0
while i < count:
u = User(email=fake.email(),
username=fake.user_name(), password="password", confirmed=True,
name=fake.name(), location=fake.city(), about_me=fake.text(),
member_since=fake.past_date())
db.session.add(u)
try:
db.session.commit()
i += 1
# 如果它不幸產生重複的資料,當你送出資料庫時,會得到IntegrityError異常,此時會回復session,取消重複的使用者
except IntegrityError:
db.session.rollback()
def posts(count=100):
fake = Faker()
user_count = User.query.count()
for i in range(count):
# offset()這個過濾器會丟棄以引數傳入的結果數量,藉由設定隨機的偏移值再呼叫first()
u = User.query.offset(randint(0,
user_count - 1)).first()
p = Post(body=fake.text(),
timestamp=fake.past_date(), author=u)
db.session.add(p)
db.session.commit()
l 於Flask Shell產生偽造的使用者與部落格文章:
flask shell
from app import fake
fake.users(100)
fake.posts(100)
l config.py:
# ...
class Config:
FLASKY_POSTS_PER_PAGE = 20
# ...
l flasky.py:
# ...
from app.models import User, Role, Permission, Post
# ...
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User,
Role=Role, Permission=Permission, Post=Post)
# ...
l app/models.py:
# ...
class User(UserMixin, db.Model):
# ...
posts =
db.relationship("Post", backref="author",
lazy="dynamic")
# ...
class Post(db.Model):
__tablename__ = "posts"
id = db.Column(db.Integer,
primary_key=True)
body = db.Column(db.Text)
timestamp = db.Column(db.DateTime,
index=True, default=datetime.utcnow)
author_id = db.Column(db.Integer,
db.ForeignKey("users.id"))
l app/main/forms.py:
# ...
class PostForm(FlaskForm):
body = TextAreaField("What's on
your mind?", validators=[DataRequired()])
submit =
SubmitField("Submit")
l app/main/views.py:
# ...
@main.route("/", methods=["GET", "POST"])
def index():
form = PostForm()
if current_user.can(Permission.WRITE)
and form.validate_on_submit():
post = Post(body=form.body.data, author=current_user._get_current_object())
db.session.add(post)
db.session.commit()
return
redirect(url_for(".index"))
# 如果沒有取得頁數,我們就使用預設的頁1
page =
request.args.get("page", 1, type=int)
# Flask-SQLAlchemy的paginate()方法的第一個且唯一必須的引數是頁數
# error_out設為False會用一個空的項目串列來回傳超出有效範圍的網頁,而非404錯誤
pagination =
Post.query.order_by(Post.timestamp.desc()).paginate(page=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, pagination=pagination)
@main.route("/user/<username>")
def user(username):
user = User.query.filter_by(username=username).first_or_404()
page =
request.args.get("page", 1, type=int)
pagination =
user.posts.order_by(Post.timestamp.desc()).paginate(page=page,
per_page=current_app.config["FLASKY_POSTS_PER_PAGE"],
error_out=False)
posts = pagination.items
return
render_template("user.html", user=user, posts=posts, pagination=pagination)
# ...
l app/templates/index.html:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {% if
current_user.is_authenticated %}{{ current_user.username }}{% else %}Stranger{%
endif %}!</h1>
</div>
<div>
{% if current_user.can(Permission.WRITE)
%}
{{ wtf.quick_form(form) }}
{% endif %}
</div>
{% include '_posts.html' %}
{% if pagination %}
<div class="pagination">
{{
macros.pagination_widget(pagination, '.index') }}
</div>
{% endif %}
{% endblock %}
l app/templates/_macros.html:
{% macro pagination_widget(pagination, endpoint) %}
<ul class="pagination">
<li{% if not pagination.has_prev
%} class="disabled"{% endif %}>
<a href="{% if
pagination.has_prev %}{{ url_for(endpoint, page=pagination.prev_num, **kwargs)
}}{% else %}#{% endif %}">
<!-- «是跳脫字元<<的意思 -->
«
</a>
</li>
{% for p in pagination.iter_pages()
%}
{% if p %}
<!-- 目前的頁數 -->
{% if p == pagination.page %}
<li class="active">
<a href="{{
url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
</li>
{% else %}
<li>
<a href="{{
url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
</li>
{% endif %}
{% else %}
<!-- …是跳脫字元…的意思 -->
<li
class="disabled"><a
href="#">…</a></li>
{% endif %}
{% endfor %}
<li{% if not pagination.has_next
%} class="disabled"{% endif %}>
<a href="{% if pagination.has_next
%}{{ url_for(endpoint, page=pagination.next_num, **kwargs) }}{% else %}#{%
endif %}">
<!-- »是跳脫字元>>的意思 -->
»
</a>
</li>
</ul>
{% endmacro %}
l app/templates/_posts.html:
<ul class="posts">
{% for post in posts %}
<li class="post">
<!-- 頭像部分 -->
<div
class="post-thumbnail">
<a href="{{
url_for('.user', username=post.author.username) }}">
<img
class="img-rounded profile-thumbnail" src="{{ post.author.gravatar(size=40)
}}">
</a>
</div>
<!-- 文字部分 -->
<div
class="post-content">
<div
class="post-date">{{ moment(post.timestamp).fromNow()
}}</div>
<div
class="post-author"><a href="{{ url_for('.user',
username=post.author.username) }}">{{ post.author.username
}}</a></div>
<div
class="post-body">{{ post.body }}</div>
</div>
</li>
{% endfor %}
</ul>
l app/templates/user.html:
<!-- ... -->
{% block page_content %}
<!-- ... -->
<h3>Posts by {{ user.username }}</h3>
{% include '_posts.html' %}
{% if pagination %}
<div class="pagination">
{{
macros.pagination_widget(pagination, '.user', username=user.username) }}
</div>
{% endif %}
{% endblock %}
l Flask-SQLALchemy分頁物件屬性:
屬性 |
說明 |
items |
在目前分頁內的項目 |
query |
被分頁的查詢指令 |
page |
目前的頁數 |
prev_num |
上一頁的頁數 |
next_num |
下一頁的頁數 |
has_next |
如果有下一頁則為True |
has_prev |
如果有上一頁則為True |
pages |
查詢得到的總頁數 |
per_page |
每頁的項目數量 |
total |
查詢指令回傳的總項目數量 |
l Flask-SQLALchemy分頁物件方法:
方法 |
說明 |
iter_pages(left_edge=2, left_current=2,
right_current=5, right_edge=2) |
例如在總共100頁的第50頁,它會回傳這些頁面:1、2、None、48、49、50、51、52、53、54、55、None、99、100 |
prev() |
上一頁的分頁物件 |
next() |
下一頁的分頁物件 |
第十一章、部落格文章-豐富文字格式(11e、11f)
l 豐富文字格式:
git checkout 11e
git checkout 11f
l 使用Markdown與Flask-PageDown來製作豐富文字文章:
PageDown是用JavaScript寫成的用戶端Markdown至HTML轉換器。
Flask-PageDown是供Flask使用的PageDown包裝器,可整合PageDown與Flask-WTF表單。
Markdown是以Python實作的伺服器端Markdown至HTML轉換器。
Bleach是以Python實作的HTML消毒器(Sanitizer)。
pip install flask-pagedown markdown bleach
l app/__init__.py:
from flask_pagedown import PageDown
# ...
pagedown = PageDown()
# ...
def create_app(config_name):
# ...
pagedown.init_app(app)
# ...
l app/models.py:
# ...
from markdown import markdown
import bleach
# ...
class Post(db.Model):
# ...
body_html = db.Column(db.Text)
# ...
@staticmethod
def on_changed_body(target, value,
oldvalue, initiator):
# markdown()函式會初步將內文轉換成HTML,clean()函式會將不在白名單上的所有標籤移除,linkify()函式可將以一般文字寫成的URL轉換成適當的<a>連結
allowed_tags = ["a",
"abbr", "acronym", "b", "blockquote",
"code", "em", "i", "li",
"ol", "pre", "strong", "ul",
"h1", "h2", "h3", "p"]
target.body_html =
bleach.linkify(bleach.clean(markdown(value, output_format="html"),
tags=allowed_tags, strip=True))
# 將on_changed_body()函式設成body的SQLAlchemy "set"事件的監聽器,每當body欄位被設為新值時,它就會被自動呼叫
db.event.listen(Post.body, "set", Post.on_changed_body)
l app/main/forms.py:
# ...
from flask_pagedown.fields import PageDownField
# ...
class PostForm(FlaskForm):
# 從原本的TextAreaField改為PageDownField
body = PageDownField("What's on
your mind?", validators=[DataRequired()])
submit =
SubmitField("Submit")
l app/templates/index.html:
<!-- ... -->
{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock %}
l app/templates/_posts.html:
<!-- ... -->
<div class="post-body">
{% if post.body_html %}
<!-- safe是為了告訴Jinja2不要轉譯escape HTML元素,因為Markdown生成的HTML是伺服器產生的,所以已直接轉譯 -->
{{ post.body_html | safe }}
{% else %}
{{ post.body }}
{% endif %}
</div>
<!-- ... -->
第十一章、部落格文章-部落格文章的永久連結(11g)
l 部落格文章的永久連結:
git checkout 11g
l app/main/views.py:
# ...
@main.route("/post/<int:id>")
def post(id):
post = Post.query.get_or_404(id)
return
render_template("post.html", posts=[post])
l app/templates/post.html:
{% extends "base.html" %}
{% import "_macros.html" as macros %}
{% block title %}Flasky - Post{% endblock %}
{% block page_content %}
{% include '_posts.html' %}
{% endblock %}
l app/templates/_posts.html:
<!-- ... -->
<div class="post-footer">
<a href="{{ url_for('.post',
id=post.id) }}">
<span class="label
label-default">Permalink</span>
</a>
</div>
<!-- ... -->
第十一章、部落格文章-部落格文章編輯器(11h)
l 部落格文章編輯器:
git checkout 11h
l app/main/views.py:
# ...
@main.route("/edit/<int:id>", methods=["GET",
"POST"])
@login_required
def edit(id):
post = Post.query.get_or_404(id)
if current_user != post.author and
not current_user.can(Permission.ADMIN):
abort(403)
form = PostForm()
if form.validate_on_submit():
post.body = form.body.data
db.session.add(post)
db.session.commit()
flash("The post has been
updated.")
return
redirect(url_for(".post", id=post.id))
form.body.data = post.body
return
render_template("edit_post.html", form=form)
l app/templates/edit_post.html:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Edit Post{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Edit Post</h1>
</div>
<div>
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock %}
l app/templates/_posts.html:
<!-- ... -->
<div class="post-footer">
{% if current_user == post.author %}
<a href="{{ url_for('.edit',
id=post.id) }}">
<span class="label
label-primary">Edit</span>
</a>
{% elif
current_user.is_administrator() %}
<a href="{{ url_for('.edit',
id=post.id) }}">
<span class="label
label-danger">Edit [Admin]</span>
</a>
{% endif %}
<a href="{{ url_for('.post',
id=post.id) }}">
<span class="label
label-default">Permalink</span>
</a>
</div>
<!-- ... -->
第十二章、追隨者-資料庫與應用程式中的追蹤者(12a、12b)
l 資料庫與應用程式中的追蹤者:
git checkout 12a
git checkout 12b
l SQLAlchemy多對多關係範例:
# 完全受SQLAlchemy管理的內部資料表
registrations = db.Table("registrations",
db.Column("student_id",
db.Integer, db.ForeignKey("students.id")),
db.Column("class_id",
db.Integer, db.ForeignKey("classes.id"))
)
class Student(db.Model):
id = db.Column(db.Integer,
primary_key=True)
name = db.Column(db.String)
classes =
db.relationship("Class", secondary= registrations,
backref=db.backref("students", lazy="dynamic"),
lazy="dynamic")
class Class(db.Model):
id = db.Column(db.Integer,
primary_key=True)
name = db.Column(db.String)
# 假設有位學生s與一門課程c,幫該位學生註冊該堂課
s.classes.append(c)
db.session.add(s)
# 列出學生s註冊的課程以及列出註冊c課程的學生名單
s.classes.all()
c.students.all()
# 學生s決定不修習課程c
s.classes.remove(c)
l app/models.py:
# ...
# 本章追隨者為自我參考(self-referential)關係,以模型來製作追隨關聯表
class Follow(db.Model):
__tablename__ = "follows"
# 若u1追蹤u2,此記錄u1的ID
follower_id = db.Column(db.Integer,
db.ForeignKey("users.id"), primary_key=True)
# 若u1追蹤u2,此記錄u2的ID
followed_id = db.Column(db.Integer,
db.ForeignKey("users.id"), primary_key=True)
timestamp = db.Column(db.DateTime,
default=datetime.utcnow)
class User(UserMixin, db.Model):
# ...
# 用兩個一對多關係來製作多對多關係
# 必須以foreign_keys引數在各個關係中指定該使用哪個外鍵,以去除外鍵的不明確
# lazy="joined"會讓join查詢指令立刻載入相關物件,如果設為預設值select,被第一次讀取時會惰性載入(loaded lazily),而且各個屬性都需要使用個別的查詢指令
# 使用all, delete-orphan值可啟用預設的cascade選項,並加入刪除孤兒紀錄的行為,因為預設的cascade行為是將它連結的任何物件的外鍵設為null
# 被自己追蹤的人
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")
# ...
# 追蹤對方
def follow(self, user):
if not self.is_following(user):
f = Follow(followed=user)
self.followed.append(f)
# 取消追蹤對方
def unfollow(self, user):
f =
self.followed.filter_by(followed_id=user.id).first()
if f:
self.followed.remove(f)
def is_following(self, user):
if user.id is None:
return False
return
self.followed.filter_by(followed_id=user.id).first() is not None
def is_followed_by(self, user):
if user.id is None:
return False
return
self.followers.filter_by(follower_id=user.id).first() is not None
l 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)
db.session.commit()
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)
db.session.commit()
flash("You are not following %s
anymore." % username)
return
redirect(url_for(".user", username=username))
# 顯示追蹤自己的人
@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=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=page,
per_page=current_app.config["FLASKY_FOLLOWERS_PER_PAGE"],
error_out=False)
follows = [{"user":
item.followed, "timestamp": item.timestamp} for item in
pagination.items]
return
render_template("followers.html", user=user, title="Followed
by", endpoint=".followed_by", pagination=pagination,
follows=follows)
l 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 %}
<!-- ... -->
l app/templates/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 %}
<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>
{% endfor %}
</table>
<div class="pagination">
{{
macros.pagination_widget(pagination, endpoint, username = user.username) }}
</div>
{% endblock %}
l tests/test_user_model.py:
import unittest
import time
from datetime import datetime
from app import create_app, db
from app.models import User, AnonymousUser, Role, Permission, Follow
class UserModelTestCase(unittest.TestCase):
# ...
def test_follows(self):
u1 =
User(email="john@example.com", password="cat")
u2 =
User(email="susan@example.org", password="dog")
db.session.add(u1)
db.session.add(u2)
db.session.commit()
self.assertFalse(u1.is_following(u2))
self.assertFalse(u1.is_followed_by(u2))
timestamp_before =
datetime.utcnow()
u1.follow(u2)
db.session.add(u1)
db.session.commit()
timestamp_after =
datetime.utcnow()
self.assertTrue(u1.is_following(u2))
self.assertFalse(u1.is_followed_by(u2))
self.assertTrue(u2.is_followed_by(u1))
self.assertTrue(u1.followed.count() == 1)
self.assertTrue(u2.followers.count()
== 1)
f = u1.followed.all()[-1]
self.assertTrue(f.followed == u2)
self.assertTrue(timestamp_before
<= f.timestamp <= timestamp_after)
f = u2.followers.all()[-1]
self.assertTrue(f.follower == u1)
u1.unfollow(u2)
db.session.add(u1)
db.session.commit()
self.assertTrue(u1.followed.count() == 0)
self.assertTrue(u2.followers.count() == 0)
self.assertTrue(Follow.query.count() == 0)
u2.follow(u1)
db.session.add(u1)
db.session.add(u2)
db.session.commit()
db.session.delete(u2)
db.session.commit()
self.assertTrue(Follow.query.count() == 0)
第十二章、追隨者-追蹤的部落格文章(12c、12d)
l 追蹤的部落格文章:
git checkout 12c
git checkout 12d
l join(結合)操作會接收兩個或以上的資料表,找出滿足指定條件的所有資料列組合,並將結合後的資料列插入一個暫時性的資料表,它就是join的結果:
# db.session.query(Post)指明它是一個會回傳Post物件的查詢指令
# select_from(Follow)指出這個查詢指令從Follow模型開始處理
# filter_by(follower_id=self.id)使用追隨者來過濾follows表
# join(Post, Follow.followed_id == Post.author_id)結合filter_by()的結果與Post物件
return
db.session.query(Post).select_from(Follow).filter_by(follower_id=self.id).join(Post,
Follow.followed_id == Post.author_id)
# 簡化上述這個指令
return Post.query.join(Follow, Follow.followed_id == Post.author_id).filter(Follow.follower_id
== seld.id)
l app/models.py:
# ...
class User(UserMixin, db.Model):
# ...
# 事後讓使用者成為他們自己的追隨者
# 於Flask Shell下指令,User.add_self_follows()
@staticmethod
def add_self_follows():
for user in User.query.all():
if not user.is_following(user):
user.follow(user)
db.session.add(user)
db.session.commit()
def __init__(self, **kwargs):
# ...
# 在剛建立使用者時,讓他們成為自己的追隨者
self.follow(self)
# ...
# 取得被追隨的文章
@property
def followed_posts(self):
return Post.query.join(Follow,
Follow.followed_id == Post.author_id).filter(Follow.follower_id == self.id)
l app/main/views.py:
# ...
# 顯示所有的或追隨的文章
@main.route("/", methods=["GET", "POST"])
def index():
form = PostForm()
if current_user.can(Permission.WRITE)
and form.validate_on_submit():
post = Post(body=form.body.data,
author=current_user._get_current_object())
db.session.add(post)
db.session.commit()
return
redirect(url_for(".index"))
page =
request.args.get("page", 1, type=int)
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=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)
# 選擇所有的文章
@main.route("/all")
@login_required
def show_all():
# cookie只能在回應物件上設定,所以這些路由必須用make_response()建立回應物件
resp =
make_response(redirect(url_for(".index")))
# set_cookie()函式的前兩個引數可接收cookie名稱與值,max_age選用引數可設定cookie還有多少秒會過期,不使用這個引數時,cookie會在瀏覽器視窗關閉時過期
resp.set_cookie("show_followed", "",
max_age=30*24*60*60)
return resp
# 選擇追隨對象的文章
@main.route("/followed")
@login_required
def show_followed():
resp =
make_response(redirect(url_for(".index")))
resp.set_cookie("show_followed", "1",
max_age=30*24*60*60)
return resp
l 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') }}">Followed</a></li>
{% endif %}
</ul>
{% include '_posts.html' %}
</div>
<!-- ... -->
第十三章、部落格文章評論-使用者評論(13a)
l 使用者評論:
git checkout 13a
l app/models.py:
# ...
from datetime import datetime
from markdown import markdown
import bleach
from flask_login import UserMixin, AnonymousUserMixin
from . import db, login_manager
# ...
# 從使用者到評論的一對多關係
class User(UserMixin, db.Model):
# ...
comments = db.relationship("Comment",
backref="author", lazy="dynamic")
# ...
# 從文章到評論的一對多關係
class Post(db.Model):
# ...
comments =
db.relationship("Comment", backref="post",
lazy="dynamic")
# ...
class Comment(db.Model):
__tablename__ = "comments"
id = db.Column(db.Integer,
primary_key=True)
body = db.Column(db.Text)
body_html = db.Column(db.Text)
timestamp = db.Column(db.DateTime,
index=True, default=datetime.utcnow)
disabled = db.Column(db.Boolean)
author_id = db.Column(db.Integer,
db.ForeignKey("users.id"))
post_id = db.Column(db.Integer,
db.ForeignKey("posts.id"))
@staticmethod
def on_changed_body(target, value,
oldvalue, initiator):
allowed_tags = ["a",
"abbr", "acronym", "b", "code",
"em", "i", "strong"]
target.body_html =
bleach.linkify(bleach.clean(markdown(value, output_format="html"),
tags=allowed_tags, strip=True))
db.event.listen(Comment.body, "set", Comment.on_changed_body)
l app/main/forms.py:
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, BooleanField, SelectField,
SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp
# ...
class CommentForm(FlaskForm):
body = StringField("Enter your
comment", validators=[DataRequired()])
submit =
SubmitField("Submit")
l app/main/views.py:
from flask import render_template, redirect, url_for, abort, flash, request,
current_app, make_response
from flask_login import login_required, current_user
from . import main
from .forms import EditProfileForm, EditProfileAdminForm, PostForm, CommentForm
from .. import db
from ..models import Permission, Role, User, Post, Comment
# ...
# 支援部落格文章評論
@main.route("/post/<int:id>", methods=["GET",
"POST"])
def post(id):
post = Post.query.get_or_404(id)
form = CommentForm()
if form.validate_on_submit():
comment =
Comment(body=form.body.data, post=post, author=current_user._get_current_object())
db.session.add(comment)
db.session.commit()
flash("Your comment has been
published.")
# page = -1這是特殊的頁碼,它會請求評論的最後一頁
return
redirect(url_for(".post", id=post.id, page=-1))
page =
request.args.get("page", 1, type=int)
if page == -1:
page = (post.comments.count() -
1) // current_app.config["FLASKY_COMMENTS_PER_PAGE"] + 1
pagination =
post.comments.order_by(Comment.timestamp.asc()).paginate(page=page, per_page=current_app.config["FLASKY_COMMENTS_PER_PAGE"],
error_out=False)
comments = pagination.items
return
render_template("post.html", posts=[post], form=form,
comments=comments, pagination=pagination)
l app/templates/post.html:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}
{% block title %}Flasky - Post{% endblock %}
{% block page_content %}
{% include '_posts.html' %}
<h4 id="comments">Comments</h4>
{% if current_user.can(Permission.COMMENT) %}
<div class="comment-form">
{{ wtf.quick_form(form) }}
</div>
{% endif %}
{% include '_comments.html' %}
{% if pagination %}
<div class="pagination">
{{
macros.pagination_widget(pagination, '.post', fragment='#comments',
id=posts[0].id) }}
</div>
{% endif %}
{% endblock %}
l app/templates/_posts.html:
<!-- ... -->
<a href="{{ url_for('.post', id=post.id) }}#comments">
<span class="label
label-primary">{{ post.comments.count() }} Comments</span>
</a>
<!-- ... -->
l app/templates/_comments.html:
<ul class="comments">
{% for comment in comments %}
<li class="comment">
<div
class="comment-thumbnail">
<a href="{{
url_for('.user', username=comment.author.username) }}">
<img
class="img-rounded profile-thumbnail" src="{{
comment.author.gravatar(size=40) }}">
</a>
</div>
<div
class="comment-content">
<div
class="comment-date">{{ moment(comment.timestamp).fromNow()
}}</div>
<div
class="comment-author"><a href="{{ url_for('.user',
username=comment.author.username) }}">{{ comment.author.username
}}</a></div>
<div
class="comment-body">
{% if comment.body_html
%}
{{ comment.body_html
| safe }}
{% else %}
{{ comment.body }}
{% endif %}
</div>
</div>
</li>
{% endfor %}
</ul>
第十三章、部落格文章評論-評論審核(13b)
l 評論審核:
git checkout 13b
l app/main/views.py:
@main.route("/moderate")
@login_required
@permission_required(Permission.MODERATE)
def moderate():
page =
request.args.get("page", 1, type=int)
pagination =
Comment.query.order_by(Comment.timestamp.desc()).paginate(page=page,
per_page=current_app.config["FLASKY_COMMENTS_PER_PAGE"],
error_out=False)
comments = pagination.items
return
render_template("moderate.html", comments=comments,
pagination=pagination, page=page)
# 將評論解除封鎖
@main.route("/moderate/enable/<int:id>")
@login_required
@permission_required(Permission.MODERATE)
def moderate_enable(id):
comment = Comment.query.get_or_404(id)
comment.disabled = False
db.session.add(comment)
db.session.commit()
return
redirect(url_for(".moderate", page=request.args.get("page",
1, type=int)))
# 將評論封鎖
@main.route("/moderate/disable/<int:id>")
@login_required
@permission_required(Permission.MODERATE)
def moderate_disable(id):
comment =
Comment.query.get_or_404(id)
comment.disabled = True
db.session.add(comment)
db.session.commit()
return
redirect(url_for(".moderate", page=request.args.get("page",
1, type=int)))
l app/templates/base.html:
<!-- ... -->
{% if current_user.can(Permission.MODERATE) %}
<li><a href="{{ url_for('main.moderate') }}">Moderate
Comments</a></li>
{% endif %}
<!-- ... -->
l app/templates/moderate.html:
<!-- 評論審核模板 -->
{% extends "base.html" %}
{% import "_macros.html" as macros %}
{% block title %}Flasky - Comment Moderation{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Comment
Moderation</h1>
</div>
<!-- 使用Jinja2的set指令來定義模板變數moderate並將它設為True,因為_comments.html模板使用這個變數來確定是否需要轉譯審核功能 -->
{% set moderate = True %}
{% include '_comments.html' %}
{% if pagination %}
<div class="pagination">
{{
macros.pagination_widget(pagination, '.moderate') }}
</div>
{% endif %}
{% endblock %}
l app/templates/_comments.html:
<!-- 轉譯評論內文 -->
<ul class="comments">
{% for comment in comments %}
<li class="comment">
<div
class="comment-thumbnail">
<a href="{{
url_for('.user', username=comment.author.username) }}">
<img class="img-rounded
profile-thumbnail" src="{{ comment.author.gravatar(size=40)
}}">
</a>
</div>
<div
class="comment-content">
<div
class="comment-date">{{ moment(comment.timestamp).fromNow()
}}</div>
<div
class="comment-author"><a href="{{ url_for('.user',
username=comment.author.username) }}">{{ comment.author.username
}}</a></div>
<div
class="comment-body">
{% if comment.disabled %}
<p><i>This
comment has been disabled by a moderator.</i></p>
{% endif %}
{% if moderate or not
comment.disabled %}
{% if
comment.body_html %}
{{
comment.body_html | safe }}
{% else %}
{{ comment.body
}}
{% endif %}
{% endif %}
</div>
{% if moderate %}
<br>
{% if comment.disabled %}
<a class="btn
btn-default btn-xs" href="{{ url_for('.moderate_enable',
id=comment.id, page=page) }}">Enable</a>
{% else %}
<a class="btn
btn-danger btn-xs" href="{{ url_for('.moderate_disable',
id=comment.id, page=page) }}">Disable</a>
{% endif %}
{% endif %}
</div>
</li>
{% endfor %}
</ul>
沒有留言:
張貼留言