Flask官方Example分析(四)--minitwit

所有例子代碼均來自于Flask的 7fca843b5f 版本

為了學(xué)習(xí)flask框架昆婿,我決定開始學(xué)習(xí)flask在GitHub上給出的官方example來熟悉flask的使用方法默色,在此版本中包含blueprintexample逊笆,flaskr泪勒,jqueryexample,minitwit這四個(gè)例子起暮,今天分析的是minitwit這個(gè)例子谒臼。

minitwit是什么?

顧名思義酬屉,minitwit就是tweet的一個(gè)mini示例版本半等。簡單地說,這是一個(gè)實(shí)現(xiàn)了注冊(cè)呐萨,登陸杀饵,發(fā)推,推文顯示谬擦,關(guān)注切距,取關(guān)等功能的flask示例。應(yīng)該說惨远,這是flask官方示例當(dāng)中python代碼量最多谜悟,最有實(shí)際意義的一個(gè)demo。

開始分析

話不多說北秽,首先來看一下文件目錄結(jié)構(gòu):

minitwit/
    minitwit/
        static/
            style.css
        templates/
            layout.html
            login.html
            regester.html
            timeline.html
        __init__.py
        minitwit.py
        schema.sql
    tests/
        test_minitwit.py
    .gitignore
    MANIFEST.in
    README
    setup.cfg
    setup.py

為了保持精力去關(guān)注核心部分葡幸,這里只去關(guān)注minitwit文件下的部分,而不去關(guān)注test部分(這并不意味著test本身沒有價(jià)值羡儿,恰恰相反礼患,TDD是一種非常有效,并且非常常用的開發(fā)組織流程,而本例使用的pytest也幾乎可以說是目前最好的python通用測(cè)試框架)缅叠,和setup安裝部分悄泥。因?yàn)樗鼈兒秃诵拇a部分的義務(wù)并沒有太大的關(guān)聯(lián)。

import部分

首先來看minitwit.py文件部分:

import time
from sqlite3 import dbapi2 as sqlite3
from hashlib import md5
from datetime import datetime
from flask import Flask, request, session, url_for, redirect, render_template, abort, g, flash, _app_ctx_stack
from werkzeug import check_password_hash, generate_password_hash

這是這個(gè)項(xiàng)目引入部分肤粱。
簡單看一下弹囚,有時(shí)間處理相關(guān)庫time和datetime,數(shù)據(jù)庫的處理庫sqlite3(常用的還有mysqlDB领曼,或者使用一種ORM來簡化相關(guān)的SQL操作)鸥鹉,hash處理庫hashlib(一般用來加密)還有flask庫里的一些api,最后庶骄,從werkzeug引入密碼的生成和檢查相關(guān)api毁渗。

配置和初始化

接下來看一下配置項(xiàng)的部分:

DATABASE = '/tmp/minitwit.db'
PER_PAGE = 30
DEBUG = True
SECRET_KEY = 'development key'

這里是配置部分,主要定義了數(shù)據(jù)庫的url連接位置单刁,推文分頁每頁的推文條數(shù)灸异,打開debug選項(xiàng)以輸出必要的調(diào)試信息(一般用于開發(fā)階段,實(shí)際生產(chǎn)環(huán)境中debug應(yīng)該處于關(guān)閉狀態(tài))羔飞,最后設(shè)置了一個(gè)秘鑰(在后面會(huì)用到)肺樟。

之后是初始化的部分:

app =Flask(__name__)
app.config.from_object(__name__)
app.config.from_envvar('MINITWIT_SETTINGS', silent=True)

這里顯示實(shí)例化一個(gè)Flask類,綁定到app上逻淌,之后從環(huán)境變量和name中引入相應(yīng)的配置項(xiàng)么伯。(這也是一般的做法:將配置信息與業(yè)務(wù)處理的代碼分開,這樣更有益于保護(hù)配置信息不被泄露卡儒,同時(shí)田柔,也更符合模塊化的需求)。

數(shù)據(jù)庫部分和一些輔助方法

接下來先看數(shù)據(jù)庫的部分:

def get_db():    
    top = _app_ctx_stack.top    
    if not hasattr(top, 'sqlite_db'):        
        top.sqlite_db = sqlite3.connect(app.config['DATABASE'])  
        top.sqlite_db.row_factory = sqlite3.Row    
    return top.sqlite_db

@app.teardown_appcontext
def close_database(exception):    
    top = _app_ctx_stack.top    
    if hasattr(top, 'sqlite_db'):        
        top.sqlite_db.close()

def init_db():   
    db = get_db()    
    with app.open_resource('schema.sql', mode='r') as f:  
        db.cursor().executescript(f.read())    
    db.commit()

@app.cli.command('initdb')
def initdb_command():    
    init_db()    
    print('Initialized the database.')

def query_db(query, args=(), one=False):   
    cur = get_db().execute(query, args)    
    rv = cur.fetchall()    
    return (rv[0] if rv else None) if one else rv

get_db和close_database分別定義了數(shù)據(jù)庫的鏈接操作和在app上下文結(jié)束的時(shí)候(即應(yīng)用關(guān)閉的時(shí)候)執(zhí)行數(shù)據(jù)庫的關(guān)閉操作骨望。

之后是init_db凯楔,引入schema.sql中的sql語句,進(jìn)行了初步的數(shù)據(jù)庫建表工作锦募,這里用到了user,follower,message三個(gè)表。隨后為了方便調(diào)用邻遏,將initdb引入命令行當(dāng)中糠亩。

最后,定義了數(shù)據(jù)庫查詢的操作query_db准验。

接下來赎线,看一下都有哪些輔助方法:

def get_user_id(username):    
    rv = query_db('select user_id from user where username = ?',[username], one=True)    
    return rv[0] if rv else None

def format_datetime(timestamp):        
    return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M')

def gravatar_url(email, size=80):    
    return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' %(md5(email.strip().lower().encode('utf-8')).hexdigest(), size)

@app.before_request
def before_request():    
    g.user = None    
    if 'user_id' in session:        
        g.user = query_db('select * from user where user_id = ?',[session['user_id']], one=True)

get_user_id定義了獲取用戶id的方法,根據(jù)用戶的username向數(shù)據(jù)庫里進(jìn)行查詢糊饱。format_datetime定義了方法垂寥,將時(shí)間戳轉(zhuǎn)化成可讀的時(shí)間表示格式。

gravatar_url是根據(jù)用戶的email,生成一個(gè)在Gravatar上對(duì)應(yīng)的頭像鏈接滞项,這里同時(shí)指定了頭像大小是80*80個(gè)像素狭归。

最后,before_request在request之前先清空全局變量g中的user文判,再向session中查詢是否有user_id过椎,如果有的話則返回相應(yīng)的用戶信息。這么做的原因是提供了cookie登陸的方法戏仓,不必要求每次都手動(dòng)輸入信息登陸疚宇。

正文部分

接下來看一下路由定義和處理的部分:

@app.route('/')
def timeline():       
    if not g.user:        
        return redirect(url_for('public_timeline'))    
    return render_template('timeline.html', messages=query_db(''' select message.*, user.* from message, user where message.author_id = user.user_id and (user.user_id = ? or user.user_id in (select whom_id from follower where who_id = ?)) order by message.pub_date desc limit ?''', [session['user_id'], session['user_id'], PER_PAGE]))

這里定義了 '/' 根路由的處理部分。如果用戶之前沒有進(jìn)行登錄操作赏殃,而且本地也沒有可用的cookie敷待,則將其重定向到公共的時(shí)間線頁面(可以按微博現(xiàn)在的邏輯理解:如果沒有登錄而去訪問微博首頁,顯示的是最新的熱門微博)仁热。如果用戶執(zhí)行了登錄操作或者本地有有效cookie榜揖,則渲染為用戶定制的時(shí)間線頁面(按時(shí)間逆序顯示用戶本人和用戶關(guān)注人所發(fā)送的推文,這點(diǎn)和微博邏輯也基本一樣:當(dāng)你登陸之后股耽,跳轉(zhuǎn)到的頁面只會(huì)顯示你關(guān)注人所發(fā)的推文根盒,默認(rèn)情況下,還會(huì)顯示本人發(fā)送的推文)物蝙。需要注意的是炎滞,這里使用了一個(gè)之前定義過的配置常量(PER_PAGE)。

之后來看一下公共的內(nèi)容定義:

@app.route('/public')
def public_timeline():       
    return render_template('timeline.html', messages=query_db('''select message.*, user.* from message, user where message.author_id = user.user_id order by message.pub_date desc limit ?''', [PER_PAGE]))

這里顯示的是為未登錄用戶(一般叫匿名用戶)定制的首頁诬乞,顯示最近來自所有用戶的推文册赛。

除了推文的顯示之外,應(yīng)該會(huì)需要一個(gè)用戶的個(gè)人頁面震嫉,或者說是個(gè)人資料頁面:

@app.route('/<username>')
def user_timeline(username):      
profile_user = query_db('select * from user where username = ?', [username], one=True)    
if profile_user is None:        
    abort(404)    
followed = False    
if g.user:        
    followed = query_db('''select 1 from follower where follower.who_id = ? and follower.whom_id = ?''', [session['user_id'], profile_user['user_id']], one=True) is not None    
return render_template('timeline.html', messages=query_db('''select message.*, user.* from message, user where user.user_id = message.author_id and user.user_id = ? order by message.pub_date desc limit ?''',  [profile_user['user_id'], PER_PAGE]), followed=followed, profile_user=profile_user)

這里用一個(gè)username部分作為動(dòng)態(tài)路由森瘪,這里的路由部分還可以這樣寫: /<string:username>
頁面部分的內(nèi)容用于顯示用戶關(guān)注者的推文票堵,如果這個(gè)username并不存在的話扼睬,則拋出404異常。

接下來是用戶的關(guān)注列表頁:

@app.route('/<username>/follow')
def follow_user(username):        
    if not g.user:        
        abort(401)    
    whom_id = get_user_id(username)    
    if whom_id is None:        
        abort(404)    
    db = get_db()    
    db.execute('insert into follower (who_id, whom_id) values (?, ?)', [session['user_id'], whom_id])    
    db.commit()    
    flash('You are now following "%s"' % username)    
    return redirect(url_for('user_timeline', username=username))

頁面主要是提供了用戶關(guān)注人的添加方法悴势,如果沒有用戶窗宇,或者用戶名不合法,則返回401404異常特纤。

既然有了用戶關(guān)注人的添加方法军俊,肯定就會(huì)有用戶關(guān)注人的取消方法(即關(guān)注與取關(guān)的邏輯關(guān)系):

@app.route('/<username>/unfollow')
def unfollow_user(username):        
    if not g.user:        
        abort(401)    
    whom_id = get_user_id(username)    
    if whom_id is None:        
        abort(404)    
    db = get_db()    
    db.execute('delete from follower where who_id=? and whom_id=?', [session['user_id'], whom_id])    
    db.commit()    
    flash('You are no longer following "%s"' % username)    
    return redirect(url_for('user_timeline', username=username))

類似關(guān)注的邏輯處理方式,這里提供了取消關(guān)注的方法捧存,并對(duì)不合法的用戶返回401和404的錯(cuò)誤信息粪躬。
其實(shí)關(guān)注和取關(guān)的操作本質(zhì)上就是對(duì)于數(shù)據(jù)庫中follower表的插入和刪除操作担败。

到此,用戶之間的關(guān)系已經(jīng)大致清楚镰官,接下來需要處理推文的部分提前。
首先是發(fā)送推文:

@app.route('/add_message', methods=['POST'])
def add_message():      
    if 'user_id' not in session:        
        abort(401)    
    if request.form['text']:        
        db = get_db()        
        db.execute('''insert into message (author_id, text, pub_date) values (?, ?, ?)''', (session['user_id'], request.form['text'], int(time.time())))        
        db.commit()        
        flash('Your message was recorded')    
    return redirect(url_for('timeline'))

首先去校驗(yàn)用戶是否已經(jīng)合法登陸,只有登錄過之后的用戶才有發(fā)送推文的權(quán)限朋魔,否則拋出401異常岖研。接下來,從表單中獲取text內(nèi)容警检,將這部分內(nèi)容插入到message表中孙援,之后重定向到時(shí)間線頁面,來顯示添加過推文信息后的新頁面扇雕。

之后定義的是用戶登錄相關(guān)的一系列操作拓售。
首先是登錄:

@app.route('/login', methods=['GET', 'POST'])
def login():       
    if g.user:        
        return redirect(url_for('timeline'))    
    error = None    
    if request.method == 'POST':        
        user = query_db('''select * from user where username = ?''', [request.form['username']], one=True)        
        if user is None:            
            error = 'Invalid username'        
        elif not check_password_hash(user['pw_hash'], request.form['password']):            
            error = 'Invalid password'        
        else:            
            flash('You were logged in')            
            session['user_id'] = user['user_id']            
            return redirect(url_for('timeline'))    
    return render_template('login.html', error=error)

如果用戶已經(jīng)登陸過,則直接將頁面重定向到時(shí)間線頁面镶奉,如果頁面的請(qǐng)求是POST方法础淤,則向user表根據(jù)username和password查詢user的信息,如果username和password正確且對(duì)應(yīng)哨苛,則將用戶登入鸽凶,并重定向到時(shí)間線頁面;如果頁面的請(qǐng)求是GET方法建峭,則顯示登陸頁面和錯(cuò)誤信息玻侥。(GET和POST方法的區(qū)別

之后是注冊(cè)頁面:

@app.route('/register', methods=['GET', 'POST'])
def register():      
    if g.user:        
        return redirect(url_for('timeline'))    
    error = None    
    if request.method == 'POST':        
        if not request.form['username']:            
            error = 'You have to enter a username'        
        elif not request.form['email'] or '@' not in request.form['email']:
            error = 'You have to enter a valid email address'        
        elif not request.form['password']:            
            error = 'You have to enter a password'        
        elif request.form['password'] != request.form['password2']:            
            error = 'The two passwords do not match'        
        elif get_user_id(request.form['username']) is not None:            
            error = 'The username is already taken'        
        else:            
            db = get_db()            
            db.execute('''insert into user (username, email, pw_hash) values (?, ?, ?)''', [request.form['username'], request.form['email'], generate_password_hash(request.form['password'])])            
            db.commit()            
            flash('You were successfully registered and can login now') 
            return redirect(url_for('login'))    
    return render_template('register.html', error=error)

這里的注冊(cè)頁面類似于此前的登陸頁面,如果用戶已經(jīng)合法登陸亿蒸,則直接重定向到時(shí)間線頁面凑兰,否則根據(jù)用戶信息,進(jìn)行校驗(yàn)之后將新用戶的信息插入user表之中(POST方法)边锁。當(dāng)收到GET請(qǐng)求時(shí)姑食,將注冊(cè)頁面和錯(cuò)誤信息一同展示出來。

最后是登出部分:

@app.route('/logout')
def logout():        
    flash('You were logged out')    
    session.pop('user_id', None)    
    return redirect(url_for('public_timeline'))

登出部分十分簡單:從session查詢對(duì)應(yīng)的用戶信息茅坛,并將該用戶的相關(guān)信息從session中彈出音半,之后彈出提示信息,并重定向到公共時(shí)間線頁面贡蓖。(其實(shí)這里應(yīng)該提前校驗(yàn)全局變量g中是否有用戶信息祟剔,即用戶是否登錄過,如果用戶沒有登錄而直接進(jìn)行登出操作摩梧,有可能session部分的操作會(huì)拋出異常)

最后是一個(gè)模板的相關(guān)操作:

app.jinja_env.filters['datetimeformat'] = format_datetime
app.jinja_env.filters['gravatar'] = gravatar_url

總結(jié)

我認(rèn)為這個(gè)demo主要涉及到的要點(diǎn)和可以改進(jìn)的有以下幾點(diǎn):

  • 用戶的登錄、注冊(cè)宣旱、登出操作
  • 已登錄用戶的信息管理(session)
  • 可以嘗試使用藍(lán)圖來更好的組織項(xiàng)目
  • 數(shù)據(jù)庫之間的關(guān)聯(lián)操作仅父,和數(shù)據(jù)庫的組織方法
  • 同個(gè)路由針對(duì)不同權(quán)限用戶(本例中是登錄用戶和匿名用戶)顯示不同的頁面內(nèi)容
  • 同個(gè)路由針對(duì)不同的請(qǐng)求方法(本例中是GET和POST)進(jìn)行不同的處理方法
  • 關(guān)于Flask的登錄部分,Flask Login是一個(gè)方便可靠的擴(kuò)展庫
  • 關(guān)于數(shù)據(jù)庫的操作部分,一個(gè)好的ORM可以大大簡化SQL操作笙纤,F(xiàn)lask里常用到的ORM是SQLAlchemy耗溜。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市省容,隨后出現(xiàn)的幾起案子抖拴,更是在濱河造成了極大的恐慌,老刑警劉巖腥椒,帶你破解...
    沈念sama閱讀 222,946評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阿宅,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡笼蛛,警方通過查閱死者的電腦和手機(jī)洒放,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滨砍,“玉大人往湿,你說我怎么就攤上這事⊥锵罚” “怎么了领追?”我有些...
    開封第一講書人閱讀 169,716評(píng)論 0 364
  • 文/不壞的土叔 我叫張陵,是天一觀的道長响逢。 經(jīng)常有香客問我绒窑,道長,這世上最難降的妖魔是什么龄句? 我笑而不...
    開封第一講書人閱讀 60,222評(píng)論 1 300
  • 正文 為了忘掉前任回论,我火速辦了婚禮,結(jié)果婚禮上分歇,老公的妹妹穿的比我還像新娘傀蓉。我一直安慰自己,他們只是感情好职抡,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,223評(píng)論 6 398
  • 文/花漫 我一把揭開白布葬燎。 她就那樣靜靜地躺著,像睡著了一般缚甩。 火紅的嫁衣襯著肌膚如雪谱净。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,807評(píng)論 1 314
  • 那天擅威,我揣著相機(jī)與錄音壕探,去河邊找鬼。 笑死郊丛,一個(gè)胖子當(dāng)著我的面吹牛晒来,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蓄坏,決...
    沈念sama閱讀 41,235評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼锥涕,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,189評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎乍炉,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滤馍,經(jīng)...
    沈念sama閱讀 46,712評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡岛琼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,775評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纪蜒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衷恭。...
    茶點(diǎn)故事閱讀 40,926評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖纯续,靈堂內(nèi)的尸體忽然破棺而出随珠,到底是詐尸還是另有隱情,我是刑警寧澤猬错,帶...
    沈念sama閱讀 36,580評(píng)論 5 351
  • 正文 年R本政府宣布窗看,位于F島的核電站,受9級(jí)特大地震影響倦炒,放射性物質(zhì)發(fā)生泄漏显沈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,259評(píng)論 3 336
  • 文/蒙蒙 一逢唤、第九天 我趴在偏房一處隱蔽的房頂上張望拉讯。 院中可真熱鬧,春花似錦鳖藕、人聲如沸魔慷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽院尔。三九已至,卻和暖如春喉誊,著一層夾襖步出監(jiān)牢的瞬間邀摆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評(píng)論 1 274
  • 我被黑心中介騙來泰國打工伍茄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留栋盹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,368評(píng)論 3 379
  • 正文 我出身青樓敷矫,卻偏偏與公主長得像贞盯,于是被迫代替她去往敵國和親音念。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,930評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 22年12月更新:個(gè)人網(wǎng)站關(guān)停躏敢,如果仍舊對(duì)舊教程有興趣參考 Github 的markdown內(nèi)容[https://...
    tangyefei閱讀 35,192評(píng)論 22 257
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,338評(píng)論 25 707
  • 第二部分 Blog例子 第八章 用戶驗(yàn)證 大部分程序需要追蹤用戶身份。當(dāng)用戶連接到程序整葡,通過一系列步驟使自己的身份...
    易木成華閱讀 1,296評(píng)論 0 4
  • 誰沒有一個(gè)能和別人,講三天三夜的故事呢俱萍。 有人突然就帶著行李住進(jìn)你的心里了端壳,她先是在你心里筑起了一道圍墻,總不能讓...
    TiKeeSi閱讀 191評(píng)論 0 0
  • 如有來生 我愿為一棵樹 春來秋去枝葉繁茂 驕陽透過綠葉 零星的光點(diǎn) 落在你的酒窩 淺白色的碎花裙 隨旋轉(zhuǎn)的舞步跳躍...
    文溯閱讀 284評(píng)論 0 1