Date: May. 26th, 2019
Lecturer: Chia
Step 1: Begin with Flask
Step 2: Template
Step 3: Web Form
Step 4: Database
Step 5: Large App Structure
建立一個目錄(newproj)作為Flask的根目錄
$ mkdir flask_proj
$ cd flask_proj
建立虛擬環境命令
$ python
Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:06:47)
Type "help", "copyright", "credits" or "license" for more information.
>>>
$ pip install virtualenv
$ pip freeze
$ python -m venv [virtual-environment-name]
啟用虛擬環境命令
$ [virtual-environment-name]\Scripts\activate
(venv) $
在啟動虛擬環境後首先安裝Flask
(venv) $ pip install flask
所有的Flask App都必須建立一個app實例
from flask import Flask
app = Flask(__name__)
路由 & view 函式
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
用flask run指令啟動web server
(venv) $ set FLASK_APP=todolist.py
(venv) $ set FLASK_DEBUG=1
(venv) $ flask run
* Serving Flask app "todolist.py"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
新建一個資料夾名為templates
(venv) $ mkdir templates
templates/index.html
<!DOCTYPE html>
<html>
<head>
<title>To Do List</title>
</head>
<body>
<h1>To Do List !</h1>
</body>
</html>
路由 & view 函式 (todolist.py)
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
(venv) $ flask run
製作 404.html
製作 500.html
錯誤頁面的view函式如下:
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
製作 404.html
<!DOCTYPE html>
<html>
<head>
<title>404 - Page Not Found</title>
</head>
<body>
<h1>404 - Page Not Found</h1>
</body>
</html>
製作 500.html
<!DOCTYPE html>
<html>
<head>
<title>500 - Internal Server Error</title>
</head>
<body>
<h1>500 - Internal Server Error</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<link rel="stylesheet" type="text/css"
href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<title>{% block title %}{% endblock title %}</title>
</head>
<body class="bg-light">
<div id="wrap">
<div id="main" class="container d-flex flex-column">
{% block content %}
{% endblock content %}
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
</body>
</html>
{% extends "base.html" %}
{% block title %}To Do List{% endblock title %}
{% block content %}
<h1>To Do List !</h1>
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
{% endblock content %}
修改 404.html 並套用 base.html
修改 500.html 並套用 base.html
修改 404.html 並套用 base.html
{% extends "base.html" %}
{% block title %}404 - Page Not Found{% endblock %}
{% block content %}
<div class="page-header">
<h1>404 - Page Not Found</h1>
</div>
{% endblock %}
{% extends "base.html" %}
{% block title %}500 - Internal Server Error{% endblock %}
{% block content %}
<div class="page-header">
<h1>500 - Internal Server Error</h1>
</div>
{% endblock %}
修改 500.html 並套用 base.html
(venv) $ pip install flask-wtf
from flask_wtf import FlaskForm
app.config['SECRET_KEY'] = 'hard to guess string'
class ToDoListForm(FlaskForm):
date = DateField('Date', default=datetime.date.today(), \
format='%Y-%m-%d', validators=[DataRequired()])
item = StringField('Item', validators=[DataRequired()])
submit = SubmitField('Submit')
from wtforms import DateField, StringField, SubmitField
from wtforms.validators import DataRequired
import datetime
@app.route('/', methods=['GET', 'POST'])
def index():
date = None
item = None
form = ToDoListForm()
if form.validate_on_submit():
date = form.date.data
item = form.item.data
form.date.data = ''
form.item.data = ''
return render_template('index.html', form=form, date=date, item=item)
<form method="POST">
<h1 class='page-title mt-5' style="text-align: center;">To Do List !</h1>
<div class="form-group col-xl-5 col-md-6" style="margin: 15px auto">
{{ form.hidden_tag() }}
{{ form.date.label }}
{{ form.date(class="form-control") }}
{{ form.item.label }}
{{ form.item(class="form-control") }}
</div>
<div style="text-align: center;">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
<div class="mt-5" style="text-align: center;">
<div class="alert alert-warning" role="alert">
<h3 style="font-family: 微軟正黑體;">
{% if date %}
截止時間:{{ date }}
{% else %}
請於上方表單中,輸入任務截止的日期 & 任務名稱~
{% endif %}
</h3>
<h3 style="font-family: 微軟正黑體;">
{% if item %}
任務名稱:{{ item }}
{% endif %}
</h3>
</div>
</div>
(venv) $ pip install Flask-SQLAlchemy
from flask_sqlalchemy import SQLAlchemy
import os
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = \
'sqlite:///' + os.path.join(basedir, 'todolist.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 減少記憶體使用
db = SQLAlchemy(app)
class TaskDate(db.Model):
__tablename__ = 'taskdate'
id = db.Column(db.Integer, primary_key = True)
date = db.Column(db.Date)
def __repr__(self):
return '<TaskDate %r>' % self.date
class TaskItem(db.Model):
__tablename__ = 'taskitem'
id = db.Column(db.Integer, primary_key = True)
item = db.Column(db.String(64), unique = True, index = True)
def __repr__(self):
return '<TaskItem %r>' % self.item
@app.route('/', methods=['GET', 'POST'])
def index():
date = None
item = None
form = ToDoListForm()
if form.validate_on_submit():
# 用表單收到的item在資料庫中查詢
item = TaskItem.query.filter_by(item=form.item.data).first()
if item is None:
date = TaskDate(date=form.date.data)
db.session.add(date)
item = TaskItem(item=form.item.data)
db.session.add(item)
db.session.commit()
form.date.data = ''
form.item.data = ''
return render_template('index.html', form=form, date=date, item=item)
(venv) $ flask shell
>>> from todolist import db
>>> db.drop_all()
>>> db.create_all()
>>> from todolist import TaskDate, TaskItem
>>> TaskDate.query.all()
[]
>>> TaskItem.query.all()
[]
from flask_migrate import Migrate
migrate = Migrate(app, db)
(venv) $ pip install Flask-Migrate
(venv) $ flask db init
(venv) $ flask db migrate -m "initial migration"
C:\Users\pcsh1\flask_proj
│ config.py --- 組態設定
│ todolist.py --- 定義Flask App實例 & 協助管理App的工作
│ requirements.txt --- 套件依賴項目
│ todolist.db
│
├─app --- (app套件) Flask App - todolist
├─migrations --- 資料庫遷移腳本
├─tests --- (tests套件) 單元測試
└─VENV --- Python虛擬環境
使用結構化的方式,來建立Flask Todolist App。
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = 'hard to guess string'
SQLALCHEMY_TRACK_MODIFICATIONS = False
@staticmethod
def init_app(app):
pass
# ...
# ...
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'todolist.db')
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
'sqlite://'
#預設為【記憶體內部資料庫】,測試完成後將不保留資料。
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
# ...
# ...
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
(venv) $ pip freeze > requirements.txt
C:\Users\pcsh1\flask_proj
├─app
│ __init__.py
│ models.py --- 資料庫模型
│
├─todolist --- 藍圖:todolist
│ │ __init__.py
│ │ forms.py --- 表單物件
│ │ views.py
│ │ errors.py
│
├─static --- 靜態檔案
│ ├─ css
│ └─ js
├─templates --- 模板
│ 404.html
│ 500.html
│ base.html
│ index.html
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from config import config #匯入config.py
db = SQLAlchemy()
# ...
在結構化之下,動態套用組態的改變(建立App實例),以便後續的單元測試。
# ...
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app) #App初始化
db.init_app(app)
### 註冊主藍圖(todolist) ###
# 在這裡指派"路由"與"錯誤頁面處理函式"
return app
from . import db
class TaskDate(db.Model):
__tablename__ = 'taskdate'
id = db.Column(db.Integer, primary_key = True)
date = db.Column(db.Date)
def __repr__(self):
return '<TaskDate %r>' % self.date
class TaskItem(db.Model):
__tablename__ = 'taskitem'
id = db.Column(db.Integer, primary_key = True)
item = db.Column(db.String(64), unique = True, index = True)
def __repr__(self):
return '<TaskItem %r>' % self.item
將TaskDate, TaskItem資料表獨立出來。
C:\Users\pcsh1\flask_proj
├─app
│ __init__.py
│ models.py --- 資料庫模型
│
├─todolist --- 藍圖:todolist
│ __init__.py
│ forms.py --- 表單物件
│ views.py
│ errors.py
在結構化之下,定義 路由 & 錯誤頁面處理函式。
from flask import Blueprint
#Blueprint('藍圖名稱', 藍圖所在的模組或套件)
todolist = Blueprint('todolist', __name__)
#匯入 路由 & 錯誤頁面處理函式
from . import views, errors
# ...
### 註冊主藍圖(todolist) ###
# 會連接到 app/todolist/__init__.py
from .todolist import todolist as todolist_blueprint
app.register_blueprint(todolist_blueprint)
return app
from flask_wtf import FlaskForm
from wtforms import DateField, StringField, SubmitField
from wtforms.validators import DataRequired
import datetime
class ToDoListForm(FlaskForm):
date = DateField('Date', default=datetime.date.today(), \
format='%Y-%m-%d', validators=[DataRequired()])
item = StringField('Item', validators=[DataRequired()])
submit = SubmitField('Submit')
from flask import render_template
from . import todolist
from .. import db
from .forms import ToDoListForm #表單物件
from ..models import TaskDate, TaskItem #資料庫模型
# ...
# ...
@todolist.route('/', methods=['GET', 'POST'])
def index():
date = None
item = None
form = ToDoListForm()
if form.validate_on_submit():
# 用表單收到的item在資料庫中查詢
item = TaskItem.query.filter_by(item=form.item.data).first()
# 查無此item的話,將該date, item寫入資料庫
if item is None:
date = TaskDate(date=form.date.data)
db.session.add(date)
item = TaskItem(item=form.item.data)
db.session.add(item)
db.session.commit() #把表單所輸入的,寫進資料庫
form.date.data = ''
form.item.data = ''
return render_template('index.html', form=form, date=date, item=item)
from flask import render_template
from . import todolist
@todolist.app_errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@todolist.app_errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
import os
from app import create_app, db
from app.models import TaskDate, TaskItem
from flask_migrate import Migrate
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)
@app.shell_context_processor
def make_shell_context():
return dict(db=db, TaskDate=TaskDate, TaskItem=TaskItem)
### 單元測試 ###
# ...
透過Windows cmd,執行 flask run
(venv) $ set FLASK_APP=todolist.py
(venv) $ flask run
C:\Users\pcsh1\flask_proj
├─tests
│ test_basics.py
│ __init__.py --- 讓測試目錄成為有效的套件 (空檔案)
import unittest
from flask import current_app
from app import create_app, db
# ...
class BasicsTestCase(unittest.TestCase):
#在每次測試之前執行,幫測試程式建立一個類似運行中App的環境
def setUp(self):
#建立一個testing組態的App,並啟動context
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all() #為測試程式建立全新的資料庫
#在每次測試之後執行
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
#確認App實例的存在
def test_app_exists(self):
self.assertFalse(current_app is None)
#確認App在測試組態下運行
def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])
# ...
### 單元測試 ###
@app.cli.command()
def test1():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests') #會自動找【tests資料夾】
unittest.TextTestRunner(verbosity=2).run(tests)
透過Windows cmd,執行test1測試
(venv) $ flask test1