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

所有例子代码均来自于Flask的 7fca843b5f 版本

为了学习flask框架,我决定开始学习flask在GitHub上给出的官方example来熟悉flask的使用方法,在此版本中包含blueprintexample,flaskr,jqueryexample,minitwit这四个例子,今天分析的是minitwit这个例子。

minitwit是什么?

顾名思义,minitwit就是tweet的一个mini示例版本。简单地说,这是一个实现了注册,登陆,发推,推文显示,关注,取关等功能的flask示例。应该说,这是flask官方示例当中python代码量最多,最有实际意义的一个demo。

开始分析

话不多说,首先来看一下文件目录结构:

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

为了保持精力去关注核心部分,这里只去关注minitwit文件下的部分,而不去关注test部分(这并不意味着test本身没有价值,恰恰相反,TDD是一种非常有效,并且非常常用的开发组织流程,而本例使用的pytest也几乎可以说是目前最好的python通用测试框架),和setup安装部分。因为它们和核心代码部分的义务并没有太大的关联。

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

这是这个项目引入部分。
简单看一下,有时间处理相关库time和datetime,数据库的处理库sqlite3(常用的还有mysqlDB,或者使用一种ORM来简化相关的SQL操作),hash处理库hashlib(一般用来加密)还有flask库里的一些api,最后,从werkzeug引入密码的生成和检查相关api。

配置和初始化

接下来看一下配置项的部分:

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

这里是配置部分,主要定义了数据库的url连接位置,推文分页每页的推文条数,打开debug选项以输出必要的调试信息(一般用于开发阶段,实际生产环境中debug应该处于关闭状态),最后设置了一个秘钥(在后面会用到)。

之后是初始化的部分:

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

这里显示实例化一个Flask类,绑定到app上,之后从环境变量和name中引入相应的配置项。(这也是一般的做法:将配置信息与业务处理的代码分开,这样更有益于保护配置信息不被泄露,同时,也更符合模块化的需求)。

数据库部分和一些辅助方法

接下来先看数据库的部分:

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分别定义了数据库的链接操作和在app上下文结束的时候(即应用关闭的时候)执行数据库的关闭操作。

之后是init_db,引入schema.sql中的sql语句,进行了初步的数据库建表工作,这里用到了user,follower,message三个表。随后为了方便调用,将initdb引入命令行当中。

最后,定义了数据库查询的操作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的方法,根据用户的username向数据库里进行查询。format_datetime定义了方法,将时间戳转化成可读的时间表示格式。

gravatar_url是根据用户的email,生成一个在Gravatar上对应的头像链接,这里同时指定了头像大小是80*80个像素。

最后,before_request在request之前先清空全局变量g中的user,再向session中查询是否有user_id,如果有的话则返回相应的用户信息。这么做的原因是提供了cookie登陆的方法,不必要求每次都手动输入信息登陆。

正文部分

接下来看一下路由定义和处理的部分:

@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]))

这里定义了 '/' 根路由的处理部分。如果用户之前没有进行登录操作,而且本地也没有可用的cookie,则将其重定向到公共的时间线页面(可以按微博现在的逻辑理解:如果没有登录而去访问微博首页,显示的是最新的热门微博)。如果用户执行了登录操作或者本地有有效cookie,则渲染为用户定制的时间线页面(按时间逆序显示用户本人和用户关注人所发送的推文,这点和微博逻辑也基本一样:当你登陆之后,跳转到的页面只会显示你关注人所发的推文,默认情况下,还会显示本人发送的推文)。需要注意的是,这里使用了一个之前定义过的配置常量(PER_PAGE)。

之后来看一下公共的内容定义:

@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]))

这里显示的是为未登录用户(一般叫匿名用户)定制的首页,显示最近来自所有用户的推文。

除了推文的显示之外,应该会需要一个用户的个人页面,或者说是个人资料页面:

@app.route('/')
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)

这里用一个username部分作为动态路由,这里的路由部分还可以这样写: /
页面部分的内容用于显示用户关注者的推文,如果这个username并不存在的话,则抛出404异常。

接下来是用户的关注列表页:

@app.route('//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))

页面主要是提供了用户关注人的添加方法,如果没有用户,或者用户名不合法,则返回401和404异常。

既然有了用户关注人的添加方法,肯定就会有用户关注人的取消方法(即关注与取关的逻辑关系):

@app.route('//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))

类似关注的逻辑处理方式,这里提供了取消关注的方法,并对不合法的用户返回401和404的错误信息。
其实关注和取关的操作本质上就是对于数据库中follower表的插入和删除操作。

到此,用户之间的关系已经大致清楚,接下来需要处理推文的部分。
首先是发送推文:

@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'))

首先去校验用户是否已经合法登陆,只有登录过之后的用户才有发送推文的权限,否则抛出401异常。接下来,从表单中获取text内容,将这部分内容插入到message表中,之后重定向到时间线页面,来显示添加过推文信息后的新页面。

之后定义的是用户登录相关的一系列操作。
首先是登录:

@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)

如果用户已经登陆过,则直接将页面重定向到时间线页面,如果页面的请求是POST方法,则向user表根据username和password查询user的信息,如果username和password正确且对应,则将用户登入,并重定向到时间线页面;如果页面的请求是GET方法,则显示登陆页面和错误信息。(GET和POST方法的区别)

之后是注册页面:

@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)

这里的注册页面类似于此前的登陆页面,如果用户已经合法登陆,则直接重定向到时间线页面,否则根据用户信息,进行校验之后将新用户的信息插入user表之中(POST方法)。当收到GET请求时,将注册页面和错误信息一同展示出来。

最后是登出部分:

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

登出部分十分简单:从session查询对应的用户信息,并将该用户的相关信息从session中弹出,之后弹出提示信息,并重定向到公共时间线页面。(其实这里应该提前校验全局变量g中是否有用户信息,即用户是否登录过,如果用户没有登录而直接进行登出操作,有可能session部分的操作会抛出异常)

最后是一个模板的相关操作:

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

总结

我认为这个demo主要涉及到的要点和可以改进的有以下几点:

  • 用户的登录、注册、登出操作
  • 已登录用户的信息管理(session)
  • 可以尝试使用蓝图来更好的组织项目
  • 数据库之间的关联操作,和数据库的组织方法
  • 同个路由针对不同权限用户(本例中是登录用户和匿名用户)显示不同的页面内容
  • 同个路由针对不同的请求方法(本例中是GET和POST)进行不同的处理方法
  • 关于Flask的登录部分,Flask Login是一个方便可靠的扩展库
  • 关于数据库的操作部分,一个好的ORM可以大大简化SQL操作,Flask里常用到的ORM是SQLAlchemy。

你可能感兴趣的:(Flask官方Example分析(四)--minitwit)