本文目的是為了完成一個項目用到的flask基本知識,例子會逐漸加深。最好對着源碼,一步一步走。
下載源碼,運行
pip install -r requirements.txt 建立環境
python db_create.py 創建自己數據庫
python db_migrate 遷移數據庫
————————————————————————————–
flask 不僅簡介小巧,同時運用的時候十分靈活。下面簡單介紹一下如何編寫一個 flask項目。
涉及調用開發服務器,數據庫連接以及 ORM 映射,還有數據庫的遷移,模板使用。后期再完善會話,管理后台,緩存等。
一 、安裝環境
我們使用 flask web框架,並用 sqlalchemy來做數據庫映射,並使用 migrate做數據遷移。
$ pip install flask $ pip install SQLAlchemy==0.7.9 $ pip install flask-sqlalchemy $ pip install flask-migrate
$ pip install sqlalchemy-migrate
二、建立項目
flask 沒有 django 那樣原生的 manage管理工具(flask-admin可以實現,日后再說)。因此我們需要手動建立目錄。新建一個 myproject目錄,在里面建 app tmp兩個文件夾,然后在 app文件夾里面建立 static, templates 兩個文件夾,用來存儲靜態文件和模板。最后目錄結構如下:
├── app
│ ├── static
│ ├── templates
└── tmp
三、 初始化文件
建立一些文件,在 app文件里建立 __init__.py models.py views.py 然后在與app 同目錄下建立 config.py 和 run.py 此時的目錄結構如下:
(env)admin@admindeMacBook-Air:~/project/python/flask/project$ tree ├── app │ ├── static │ ├── templates │ ├── __init__.py │ ├── models.py │ ├── views.py ├── config.py ├── run.py └── tmp
四 、開始項目
1 hello wrod
打開 (/app/__init.py) 文件,寫入
# -*- coding: utf-8 -*- from flask import Flask # 引入 flask app = Flask(__name__) # 實例化一個flask 對象 import views # 導入 views 模塊 # from app import views
注意,我們的 app 文件夾其實是一個python包,from app import views 這句話也就是從 包app里面導入 views模塊,所以寫成注釋的那句話也沒錯,其他代碼實踐喜歡寫成后面那種
現在我們來開始寫我們的視圖,打開 (/app/views.py)
# -*- coding: utf-8 -*- from app import app from models import User, Post, ROLE_USER, ROLE_ADMIN @app.route(‘/’) def index(): return ‘hello world, hello flask’
下一步我們將要啟動我們的開發服務器,打開 (/run.py)
# -*- coding: utf-8 -*- from app import app if __name__ == ‘__main__’: app.debug = True # 設置調試模式,生產模式的時候要關掉debug app.run() # 啟動服務器
這段代碼需要注意第一句 from app import app。也許會有疑問,我們的 app 包里,貌似沒有 app.py 這樣的模塊。其實這是 flask 方式和python的導入方式。from app 指導入 app 包里的 __iniy__.py 所以這句話的含義是導入 __.init__.py 里面的 app實例。
打開shell 運行
$ python run.py (env)admin@admindeMacBook-Air:~/project/python/flask/project$ python run.py
* Running on http://127.0.0.1:5000/ * Restarting with reloader
用瀏覽器打開 http://127.0.0.1:5000/ 將會看到一個輸入 hello world 的頁面
2 連接數據庫
下面開始連接數據庫引擎 先要做一個簡單的配置 打開 (/config.py)
# -*- coding: utf-8 -*- import os basedir = os.path.abspath(os.path.dirname(__file__)) SQLALCHEMY_DATABASE_URI = ‘sqlite:///%s’ % os.path.join(basedir, ‘app.db’) SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, ‘db_repository’) CSRF_ENABLED = True SECRET_KEY = ‘you-will-never-guess’
SQLALCHEMY_DATABASE_URI是the Flask-SQLAlchemy必需的擴展。這是我們的數據庫文件的路徑。
SQLALCHEMY_MIGRATE_REPO 是用來存儲SQLAlchemy-migrate數據庫文件的文件夾。
然后打開 (/app/__init__.py)
# -*- coding: utf-8 -*- import os from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy from config import basedir app = Flask(__name__) app.config.from_object(‘config’) # 載入配置文件 db = SQLAlchemy(app) # 初始化 db 對象
# from app import views, models # 引用視圖和模型
import views, models
打開 (/app/models.py)
# -*- coding: utf-8 -*- from app import db ROLE_USER = 0 ROLE_ADMIN = 1
class User(db.Model): id = db.Column(db.Integer, primary_key=True) nickname=db.Column(db.String(60), index=True, unique=True) email = db.Column(db.String(120), index=True, unique=True) role = db.Column(db.SmallInteger, default=ROLE_USER)
def __repr__(self): return ‘<User %r>’ % self.nickname
我們的模型建立了一個 user類,正好是 數據庫里面的 user 表,表有四個字段
現在我們的數據庫配置已經好了,可是還沒有建立數據庫。新建一個文件 (/db_create.py)
# -*- coding: utf-8 -*- from migrate.versioning import api from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO from app import db import os.path db.create_all() if not os.path.exists(SQLALCHEMY_MIGRATE_REPO): api.create(SQLALCHEMY_MIGRATE_REPO, 'database repository') api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) else: api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, api.version(SQLALCHEMY_MIGRATE_REPO))
這個腳本是完全通用的,所有的應用路徑名都是從配置文件讀取的。當你用在自己的項目時,你可以把腳本拷貝到你的項目目錄下就能正常使用了。
打開shell 運行
$ python db_create.py
運行這條命令之后,你就創建了一個新的app.db文件。這是個支持遷移的空sqlite數據庫,同時也會生成一個帶有幾個文件的db_repository目錄,這是SQLAlchemy-migrate存儲數據庫文件的地方,注意如果數據庫已存在它就不會再重新生成了。這將幫助我們在丟失了現有的數據庫后,再次自動創建出來。
運行
slqite3 app.db
>>> .tables
>>> user
或者使用 sqlite 圖形化客戶端,沒有需要安裝 (windows 下直接下載安裝包即可)
$ sudo apt-get install sqlitebrowser
3 數據庫遷移
每當我們更改了 models 。等價於更改了數據庫的表結構,這個時候,需要數據庫同步。新建 (/db_migrate.py)
# -*- coding: utf-8 -*- import imp from migrate.versioning import api from app import db from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO migration = SQLALCHEMY_MIGRATE_REPO + '/versions/%03d_migration.py' % (api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) + 1) tmp_module = imp.new_module('old_model') old_model = api.create_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) exec old_model in tmp_module.__dict__ script = api.make_update_script_for_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, tmp_module.meta, db.metadata)
open(migration, 'wt').write(script) api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) print 'New migration saved as' + migration print 'Current database version:' + str(api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO))
這個腳本看起來很復雜,SQLAlchemy-migrate通過對比數據庫的結構(從app.db文件讀取)和models結構(從app/models.py文件讀取)的方式來創建遷移任務,兩者之間的差異將作為一個遷移腳本記錄在遷移庫中,遷移腳本知道如何應用或者撤銷一次遷移,所以它可以方便的升級或者降級一個數據庫的格式。
開始數據庫遷移
$ python db_mirgate.py
New migration saved as db_repository/versions/001_migration.py Current database version: 1
相應的,我們繼續創建數據庫 升級和回退的腳本
(/db_upgrade.py)
# -*- coding: utf-8 -*- from migrate.versioning import api from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) print 'Current database version:' + str(api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO))
(/db_downgrade.py)
# -*- coding: utf-8 -*-
from migrate.versioning import api from config import SQLALCHEMY_DATABASE_URI from config import SQLALCHEMY_MIGRATE_REPO v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) api.downgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, v – 1) print 'Current database version:' + str(api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO))
4 操作數據庫
接下來,我們定義一個新的models class 並做第二次遷移歸並
打開(/app/models.py)
# -*- coding: utf-8 -*- from app import db
ROLE_USER = 0 ROLE_ADMIN = 1 class User(db.Model): id = db.Column(db.Integer, primary_key=True) nickname=db.Column(db.String(60), index=True, unique=True) email = db.Column(db.String(120), index=True, unique=True) role = db.Column(db.SmallInteger, default=ROLE_USER) posts = db.relationship(‘Post’, db.backref = ‘author’, db.lazyload = ‘dynamic’) def __repr__(self): return ‘<User %r>’ % self.nickname class Post(db.Model): id = db.Column(db.Integer, primary_key=True) body = db.Column(db.String(140)) timestamp = db.Column(db.DateTime) user_id = db.Column(db.Integer, db.ForeignKey(‘user.id’)) def __repr__(self): return ‘<Post %r>’ % (self.body)
這里我們進行了兩個class 的關聯。
$ python db_mirgrate.py
New migration saved as db_repository/versions/002_migration.py Current database version: 2
打開 (/app/views.py)
# -*- coding: utf-8 -*- from flask import render_template, flash, redirect, session, url_for, request, g from app import app, db from models import User, Post, ROLE_USER, ROLE_ADMIN @app.route(‘/’) def index(): return render_template(‘index.html’) @app.route(‘/adduser/<nickname>/<email>’) def adduser(nickname, email): u = User(nickname=nickname, email=email) try: db.session.add(u) db.session.commit() return ‘add successful’ except Exception, e: return ‘something go wrong’ @app.route(‘/getuser/<nickname>’) def getuser(nickname): user = User.query.filter_by(nickname=nickname).first() return render_template(‘user.html’, user=user) @app.errorhandler(404) def internal_error(error): return render_template(‘404.html’), 404 @app.errorhandler(500) def internal_error(error): db.session.rollback() return render_template(‘500.html’), 500
這次我們使用了模板, 新建(/app/templates/user.html)
<html> <head> <title>user</title> </head> <body> <h1>user</h1> <ul> <li>user: {{ user.nickname }}</li> <li>email: {{ user.email }}</li> </ul> </body> </html>
最后運行
$ python run.py
用瀏覽器訪問 http://127.0.0.1:5000/adduser/username/useremail
最后還可以用 sqlite 客戶端打開查看我們的數據庫變換。關於更多的數據庫操作方法,請閱讀 sqlalchemy文檔。
最后我們的代碼目錄結構如下:
(env)admin@admindeMacBook-Air:~/project/python/flask/project$ tree . ├── app │ ├── __init__.py │ ├── models.py │ ├── static │ ├── templates │ │ ├── 404.html │ │ ├── 500.html │ │ ├── index.html │ │ └── user.html │ ├── views.py ├── app.db ├── config.py ├── db_create.py ├── db_downgrade.py ├── db_migrate.py ├── db_repository │ ├── __init__.py │ ├── manage.py │ ├── migrate.cfg │ ├── README │ └── versions │ ├── 001_migration.py │ ├── 002_migration.py │ ├── __init__.py ├── db_upgrade.py ├── requirements.txt ├── run.py ├── tmp
5 表單
接下來,我們將要接觸表單方面的內容。flask原生提供的表單處理,也比較簡單。當然,有了輪子,我們就直接用好了。需要安裝一個
Flask-WTF==0.9.4。
安裝完之后,打開 (/app/forms.py)
# -*- coding: utf-8 -*- import re from flask_wtf import Form from flask_wtf.html5 import EmailField from wtforms import TextField, PasswordField, BooleanField from wtforms.validators import DataRequired, ValidationError from models import User from hashlib import md5 class RegisterFrom(Form): nickname = TextField(‘nickname’, validators=[DataRequired()]) email = EmailField(’email’, validators=[DataRequired()]) password = PasswordField(‘password’, validators=[DataRequired()]) conform = PasswordField(‘conform’, validators=[DataRequired()]) def validate_nickname(self, field): nickname = field.data.strip() if len(nickname) < 3 or len(nickname) > 20: raise ValidationError(‘nickname must be 3 letter at least’) elif not re.search(r’^\w+$’, nickname): raise ValidationError(‘User names can contain only alphanumeric characters and underscores.’) else: # 驗證是否已經注冊 u = User.query.filter_by(nickname=nickname).first() if u: raise ValidationError(‘The nickname already exists’) def validate_email(self, field): email = field.data.strip() email = User.query.filter_by(email=email).first() if email: raise ValidationError(‘The email already exists’) def validate_password(self, field): password = field.data.strip() if len(password) < 6: raise ValidationError(‘password must be 3 letter at least’) def validate_conform(self, field): conform = field.data.strip() if self.data[‘password’] != conform: raise ValidationError(‘the password and conform are different’)
上述代碼定義了一個 RegisterFrom class,正好是一個表單,class的字段映射為表單的域。其中 wtforms 中有 TextField, PasswordField, BooleanField這些表單類型,flask_wtf.html5 中 EmailField 則是 html5 規范的。
表單有 form.validate_on_submit() 方法,這個方法執行之前會驗證表單域。驗證方法和django類似,都是 validate_field 的方法。其中一個參數 field正好是表單的value值 需要注意驗證兩次密碼是否相同的時候,需要用到 self.data 。這是一個字典,包含了表單域的name和value
6 用戶注冊
打開(/app/views.py)添加注冊方法。
@app.route(‘/account/signup’, methods=[‘GET’, ‘POST’]) def signup(): form = RegisterFrom() if request.method == ‘POST’: if form.validate_on_submit(): psdmd5 = md5(form.data[‘password’]) password = psdmd5.hexdigest() u = User(nickname=form.data[‘nickname’], email=form.data[’email’], password=password) try: db.session.add(u) db.session.commit() flash(‘signup successful’)
except Exception, e: return ‘something goes wrong’ return redirect(url_for(‘signin’)) return render_template(‘signup.html’, form=form)
模版 新建 (/app/tempaltes/signup.html)
<!– extend base layout –> {% extends “base.html” %} {% block content %} <form method=”POST” action=”> {{ form.hidden_tag() }} <p>{{ form.nickname.label }} {{ form.nickname(size=20) }}</p> <p>{{ form.email.label }} {{ form.email(size=20) }}</p> <p>{{ form.password.label }} {{ form.password(size=20) }}</p> <p>{{ form.conform.label }} {{ form.conform(size=20) }}</p> <input type=”submit” value=”Sign Up”> <input type=”reset” value=”Reset”> </form> {{ form.errors }} {% endblock %}
其中,主要是用到 form 對象的一些屬性。form.nickname.label 是指表單類定義的名字,form.nickname 會轉變成 表單域的 html 代碼 即 <input type=”text” id=”nickname” name=”nickaname” value=””/>
7 用戶登錄
先添加一個用戶登錄的表單,打開 (/app/forms.py)
class LoginForm(Form): nickname = TextField(‘nickname’, validators=[DataRequired()]) password = PasswordField(‘password’, validators=[DataRequired()]) remember_me = BooleanField(‘remember_me’, default=False) def validate_nickname(self, field): nickname = field.data.strip() if len(nickname) < 3 or len(nickname) > 20: raise ValidationError(‘nickname must be 3 letter at least’) elif not re.search(r’^\w+$’, nickname): raise ValidationError(‘User names can contain only alphanumeric characters and underscores.’) else: return nickname
用戶登錄,最簡單的方法就是 驗證用戶名,如果通過,則添加 session,登出的時候,清除session即可,模版里面可以直接使用 request.session用來判斷用戶登錄狀態
打開(/app/views.py) 添加登錄登出方法
@app.route(‘/account/signin’, methods=[‘GET’, ‘POST’]) def signin(): form = LoginForm() if request.method == ‘POST’: if form.validate_on_submit(): nickname = form.data[‘nickname’] psdmd5 = md5(form.data[‘password’]) password = psdmd5.hexdigest() u = User.query.filter_by(nickname=nickname, password=password).first() if u: session[‘signin’] = True flash(‘signin successful’) return redirect(url_for(‘index’)) else: flash(u’用戶名或者密碼錯誤’) return render_template(‘signin.html’, form=form) @app.route(‘/account/signout’) def signout(): session.pop(‘signin’, None) flash(‘signout successful’) return redirect(‘index’)
注意,我們使用 flask 的時候,用了中文,就必須是 unicode
除了原生的登錄方式,我們還可以用flask-login。安裝flask-login
此時需要初始化一個 login_manager 對象,打開 (/app/__init__.py)
# -*- coding: utf-8 -*- import os from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy from flask.ext.login import LoginManager from config import basedir app = Flask(__name__)
app.config.from_object(‘config’) db = SQLAlchemy(app) # 初始化數據庫管理對象 login_manager = LoginManager() # 初始化登錄管理對象 login_manager.init_app(app) login_manager.login_view = “signin” # 登錄跳轉視圖 login_manager.login_message = u”Bonvolu ensaluti por uzi tio paĝo.” # 登錄跳轉視圖前的輸出消息 # from app import views, models import views, models
然后打開 (/app/views.py)
添加下面方法, 注釋掉之前的 登入登出方法
from flask.ext.login import login_user, logout_user, current_user, login_required from app import app, db, login_manager @login_manager.user_loader def load_user(userid): return User.query.get(userid) @app.route(‘/account/signin’, methods=[‘GET’, ‘POST’]) def signin(): form = LoginForm() if request.method == ‘POST’: if form.validate_on_submit(): nickname = form.data[‘nickname’] psdmd5 = md5(form.data[‘password’]) password = psdmd5.hexdigest() remember_me = form.data[‘remember_me’] user = User.query.filter_by(nickname=nickname, password=password).first() if user: login_user(user, remember = remember_me) flash(‘signin successful’) return redirect(request.args.get(“next”) or url_for(“index”)) else: flash(u’用戶名或者密碼錯誤’) return render_template(‘signin.html’, form=form) @app.route(‘/account/signout’) @login_required def signout():
logout_user() flash(‘signout successful’) return redirect(‘index’)
模版也會響應的改,具體看源碼吧。
base.html
<html> <head> <meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″ /> {% if title %} <title>{{title}} – microblog</title> {% else %} <title>microblog</title> {% endif %} </head> <body> <div class="metanav"> <div> <a href=”{{ url_for(‘index’) }}”>Home</a> {% if current_user.is_authenticated() %} <a href=”{{ url_for(‘signout’) }}”>signout</a> {% else %} <a href=”{{ url_for(‘signin’) }}”>signin</a> {% endif %} </div> {% for message in get_flashed_messages() %} <div class="flash">{{ message }}</div> {% endfor %} {% block content %}{% endblock %} </body> </html>
代碼下載: https://github.com/rsj217/flask-extend/tree/master/project