# Python Flask實作_開始建置頁面內容_08_blog_post_建立文章
###### tags: `python` `flask` `blog`
## 說明
在建立好Blog之後,要開始來建置文章內容(Post),文章會有標籤設置,在這個章節我們會先建立好相關的Model、Form與View function,後續再試著加入markdown。
### 作業說明
我們從Model開始建置,預期需要title、body、category_id(fk)、create_date、edit_date、author_id(fk)、blog_main_id(fk)、slug、flag。
:::success
* 文件:app_blog\blog\model.py
* 說明:建置Blog_Post的Model
```python=
class Blog_Post(db.Model):
"""
文章內容
title:文章標題
body:文章內容
category(fk):文章類別(可以依需求設置為多對多)
author_id(fk):作者
blog_main_id(fk):blog
create_date:建立日期
edit_date:更新日期
slug:網址
flag:是否存活(依看人需求,看要不要實際刪掉,不要的話就用flag來控制即可
"""
__tablename__ = 'Blog_Posts'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(80))
body = db.Column(db.Text)
category_id = db.Column(db.Integer, db.ForeignKey('Blog_Categorys.id'))
author_id = db.Column(db.Integer, db.ForeignKey('UserRgeisters.id'))
blog_main_id = db.Column(db.Integer, db.ForeignKey('Blog_Main.id'))
create_date = db.Column(db.DateTime, default=datetime.utcnow)
edit_date = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
slug = db.Column(db.String(256), unique=True)
flag = db.Column(db.Boolean, default=True)
def __init__(self, title, body, category, author, blog_main, slug=None):
self.title = title
self.body = body
self.category_id = category
self.author_id = author.id
self.blog_main_id = blog_main
self.slug = slug
def __repr__(self):
return '<POST> %s' % self.title
```
第22行:設置有更新就自動更新時間
:::
:::success
* 文件:app_blog\blog\model.py
* 說明:建置Blog_Category的Model
```python=
class Blog_Category(db.Model):
"""
文章類別
name:類別名稱
"""
__tablename__ = 'Blog_Categorys'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
def __init__(self, name):
self.name = name
def __repr__(self):
return '<Category> %s' % self.name
```
:::
建置好Post的Model之後,我們要設置文章(Blog_Post)與BLOG(Blog_Main)的關聯以及文章(Blog_Post)與分類(Blog_Category)的關聯
:::success
* 文件:app_blog\blog\model.py
* 說明:設置Blog_Main與Blog_Post的關聯
```python=
class Blog_Main(db.Model):
#...中略...#
# 設置blog_main與blog_post的關聯
posts = db.relationship('Blog_Post', backref='blogs', lazy='dynamic')
#...下略...#
```
:::
:::success
* 文件:app_blog\blog\model.py
* 說明:設置Blog_Post與Blog_Category的關聯
```python=
class Blog_Post(db.Model):
#...中略...#
# 設置blog_post與blog_category的關聯
categorys = db.relationship('Blog_Category', backref=db.backref('posts', lazy='dynamic'))
def __init__(self, title, body, category, author, blog_main, slug=None):
#...下略...#
```
第4行:Post與Category是多對一的概念,一個分類可以出現在多個文章,而一個文章只會有一個分類,所以Post本身不需要設置lazy=dynamic,但我們要設置給反向查詢的時候可以引用,就透過backref=db.backref(xxx)來設置。
:::
利用指令測試目前的關聯狀況是否正確,在這之前,我們需要先利用Python Shell來更新一下資料庫。
:::warning
* 測試
* 說明:更新資料庫
```shell=
python manager.py db migrate
python manager.py db upgrade
```
成功的產生兩張新的資料表,也發現之前的main名稱沒有設置好..

:::
:::warning
* 測試
* 說明:手動新增一個category
```shell=
>>> from app_blog.blog.model import Blog_Main
>>> from app_blog.blog.model import Blog_Post
>>> from app_blog.blog.model import Blog_Category
>>> from app_blog.author.model import UserRegister
>>> category = Blog_Category(name='Flask', remark='Flask_Travel')
>>> db.session.add(category)
>>> db.session.commit()
```

* 說明:取出相關物件
```shell
>>> author=UserRegister.query.first()
>>> author
username:Shaoe.chen, email:shaoe.chen@gmail.com
>>> blog=Blog_Main.query.first()
>>> blog
<blog>:Flask, <author>:1
>>> category=Blog_Category.query.first()
>>> category
<Category> Flask
```
* 說明:產生一筆post資料
```shell=
>>> post = Blog_Post('Flask_Post','Flask is very nice',category.id,author.id,blog.id)
>>> db.session.add(post)
>>> db.session.commit()
```
資料確實的寫入,Model的部份應該是可以了。

* 測試更新文章之後編輯時間會不會異動
```shell=
>>> post.body='I edit the post and check the edittime'
>>> db.session.add(post)
>>> db.session.commit()
```
在編輯文章內容之後,我們確認edit_date的時間也有異動。

:::
Model設置完成之後,可以接續著設置Form。
:::success
* 文件:app_blog\blog\form.py
* 說明:建置Blog_Post的Form
```python=
# 追加import
from wtform import SelectField
from flask_login import current_user
#...中略...#
class Form_Blog_Post(FlaskForm):
"""
建置blog文章的表單
"""
post_title = StringField('Post_Title', validators=[
validators.DataRequired(),
validators.Length(1, 80)
])
post_body = TextAreaField('Post_Body', validators=[
validators.DataRequired()
])
post_blog = SelectField('blog_main_id', coerce=int)
post_category = SelectField('category_id', coerce=int)
submit = SubmitField('Submit Post')
def __init__(self):
super(Form_Blog_Post, self).__init__()
self.post_blog.choices = self._get_blog_main()
self.post_category.choices = self._get_category()
def _get_blog_main(self):
obj = Blog_Main.query.with_entities(Blog_Main.id, Blog_Main.blog_name).filter_by(author=current_user._get_current_object().id).all()
return obj
def _get_category(self):
obj = Blog_Category.query.with_entities(Blog_Category.id, Blog_Category.name).all()
return obj
```
第2行:import SelectField
第5、8行:回傳值取得的值
[參考wtform官方文件](http://wtforms.simplecodes.com/docs/0.6/ext.html#module-wtforms.ext.sqlalchemy)
第30行:利用current_user.\_get\_current\_object來取得當前登入使用者物件
對於SelectField的choice選項部份,也可以在view中實作的時候再加入也可以。
:::
設置版面
:::success
* 文件:app_blog\templates\blog\blog_post_edit.html
* 說明:建置Blog_Post的版面
```htmlmixed=
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Blog_Post_Edit{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {{ current_user.username }}</h1>
<h2>Write Your Story</h2>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
```
版的部份沒有特別的調整,所以一定是醜醜的,請不要介意,再依自己需求調整即可。
:::
一樣的,我們利用一個簡單的View Function來確認版面的呈現是否正常。
:::success
* 文件:app_blog\blog\view.py
* 說明:建置Blog_Post的View,測試版面是否正常。
```python=
# import form
from app_blog.blog.form import Form_Blog_Main, Form_Blog_Post
#...中略...#
@blog.route('/blog_post/c', methods=['GET', 'POST'])
@login_required
def post_blog_post():
form = Form_Blog_Post()
return render_template('blog/blog_post_edit.html', form=form)
```
若有執行錯誤請看[其它說明](#query)
:::
簡單的設置View Function之後,執行專案確認狀況。
:::warning
* 測試
* 說明:確認版面以及下拉功能是否正常

透過QuerySelectField我們可以很快速的產生一個下拉選單的功能,不過記得注意這部份後續在WTForm3.0的時候預計取消,到時候可能要改用WTForm-SQLAlchemy
:::
確認版面正常,我們繼續將所有的View Function調整完善。
:::success
* 文件:app_blog\blog\view.py
* 說明:調整Blog_Post的View
```python=
# 追加import Blog_Post Model
from app_blog.blog.model import Blog_Main, Blog_Post
#...中略...#
@blog.route('/blog_post/c', methods=['GET', 'POST'])
@login_required
def post_blog_post():
form = Form_Blog_Post()
if form.validate_on_submit():
post = Blog_Post(
title=form.post_title.data,
body=form.post_body.data,
category=form.post_category.data,
author=current_user._get_current_object(),
blog_main=form.post_blog.data,
slug='%i-%i-%i-%i-%s' % (current_user.id, datetime.now().year, datetime.now().month, datetime.now().day,
slugify(form.post_title.data))
)
db.session.add(post)
db.session.commit()
flash('Blog Post Success')
return redirect(url_for('blog.post_blog_post'))
return render_template('blog/blog_post_edit.html', form=form)
```
第14行:利用current_user._get_current_object()取得user object
第16行:利用套件slugify來設置網址slug([其它說明](#slug))
:::
測試寫入狀況
:::warning
* 測試
* 連結:http://127.0.0.1:5000/blog/blog_post/c
* 說明:寫入資料

* 說明:確認資料庫寫入狀況

:::
## 總結
跟建置Blog_Main的時候一樣,雖然好像寫了很多東西,但依然是SOP,MODEL、FORM、HTML、VIEW FUNCTION。
我們利用SQLAlchemy的db.Datetime參數onupdate,讓文章有修改的時候就自動更新時間,也知道了多對一的關聯設置如果要設置反向lazy的話要利用backref=db.backref(xxx)。
另外,我們知道為什麼使用current\_user.\_get\_current\_object而不直接使用current_user.id,原因在current_user本身是proxy object,而不是user object。
利用proxy object的特性,我們也成功的在form的query中可以過濾出屬於該使用者的blog。(post_blog選單)
下一節我們會將CRUD的其它部份設置完善,再加入markdown。
## 其它
### <span id='query'>為何使用QuerySelectField會異常</span>
:::info
當wtform升至3.0之後將不再支援wtforms.ext.sqlalchemy,請特別注意。

:::
在版本上,如果你安裝了sqlalchemy 1.2 以上的版本的話,會因為回傳值的問題造成你使用QuerySelectField的unpack異常(如下圖)。(這是因為原本回傳兩個參數變三個)

這時候你可以直接調整QuerySelectField的get_pk_from_identity:
原始:
```python=
def get_pk_from_identity(obj):
cls, key = identity_key(instance=obj)
return ':'.join(text_type(x) for x in key)
```
調整後:
```python=
def get_pk_from_identity(obj):
cls, key = identity_key(instance=obj)[:2]
return ':'.join(text_type(x) for x in key)
```
[參考來源_git](https://github.com/wtforms/wtforms-sqlalchemy/issues/9)
另外,QuerySelectField看git上的討論似乎未來也會有調整,這部份要多關注,避免未來更新之後造成網頁異常。
又或者你不想亂改原始安裝的程式,也可以加入下面這段程式碼,只是一樣都是為了只取兩個值。
```python=
import wtforms.ext.sqlalchemy.fields as t
def get_pk_from_identity(obj):
cls, key = t.identity_key(instance=obj)[:2]
return ':'.join(t.text_type(x) for x in key)
t.get_pk_from_identity = get_pk_from_identity
```
[參考來源_stackoverflow](https://stackoverflow.com/questions/48353190/why-does-flask-wtforms-and-wtforms-sqlalchemy-queryselectfield-produce-too-many)
### <span id='slug'>slugify</span>
```shell=
pip install python-slugify
```
google的blogsport在產生blog的時候也是用相同的方式在處理,主要是我們希望可以設置一個固定的網址,而不是用id去做查詢(其它方式如uuid..)。但它必需是唯一,如果單純的轉置的話恐怕一個人文章叫做flask,以後就不能有這個文章title了,所以加入了id、年、月、日,如果還卡到了那也無話可說,是吧。
### 使用QuerySelectField
:::success
* 文件:app_blog\blog\form.py
* 說明:SelectField也可以改以QuerySelectField來設置
```python=
# 追加import
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from flask_login import current_user
#...中略...#
def get_category():
return Blog_Category.query
def get_blog():
return Blog_Main.query.filter_by(author=current_user._get_current_object().id)
class Form_Blog_Post(FlaskForm):
"""
建置blog文章的表單
"""
post_title = StringField('Post_Title', validators=[
validators.DataRequired(),
validators.Length(1, 80)
])
post_body = TextAreaField('Post_Body', validators=[
validators.DataRequired()
])
post_blog = QuerySelectField('Post_Blog', query_factory=get_blog, get_label='blog_name')
post_category = QuerySelectField('Post_Category', query_factory=get_category, get_label='name')
submit = SubmitField('Submit Post')
```
第2行:import wtform的延伸lib
第22、23行:利用QuerySelectField來做下拉選單(也有多選的選單)
第5、8行:回傳值取得的值
[參考wtform官方文件](http://wtforms.simplecodes.com/docs/0.6/ext.html#module-wtforms.ext.sqlalchemy)
第10行:利用current_user._get_current_object來取得當前登入使用者物件
:::