# 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名稱沒有設置好.. ![](https://i.imgur.com/NGawOoA.png) ::: :::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() ``` ![](https://i.imgur.com/iOgXx7W.png) * 說明:取出相關物件 ```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的部份應該是可以了。 ![](https://i.imgur.com/YWPwrym.png) * 測試更新文章之後編輯時間會不會異動 ```shell= >>> post.body='I edit the post and check the edittime' >>> db.session.add(post) >>> db.session.commit() ``` 在編輯文章內容之後,我們確認edit_date的時間也有異動。 ![](https://i.imgur.com/XjGv2DA.png) ::: 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 * 測試 * 說明:確認版面以及下拉功能是否正常 ![](https://i.imgur.com/VxJDy7V.png) 透過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 * 說明:寫入資料 ![](https://i.imgur.com/mIoXAyk.png) * 說明:確認資料庫寫入狀況 ![](https://i.imgur.com/6Pz8ryK.png) ::: ## 總結 跟建置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,請特別注意。 ![](https://i.imgur.com/LwxsXuu.png) ::: 在版本上,如果你安裝了sqlalchemy 1.2 以上的版本的話,會因為回傳值的問題造成你使用QuerySelectField的unpack異常(如下圖)。(這是因為原本回傳兩個參數變三個) ![](https://i.imgur.com/ZJreQNr.png) 這時候你可以直接調整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來取得當前登入使用者物件 :::