To Do List
Date: May. 26th, 2019
Lecturer: Chia
OUTLINE
Step 1: Begin with Flask
Step 2: Template
Step 3: Web Form
Step 4: Database
Step 5: Large App Structure
Step 1: Begin with Flask
- 虛擬環境
- 基本app架構 - todolist.py
- 啟動web server
- render_template - index.html
建立一個目錄(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>'
基本app架構 - todolist.py
用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)
- 打開瀏覽器,並且輸入 http://127.0.0.1:5000/ 或 http://localhost:5000/ 即可。
啟動web server
新建一個資料夾名為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>
render_template - index.html
路由 & view 函式 (todolist.py)
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
render_template - index.html
- 打開瀏覽器,重新整理網頁
- http://127.0.0.1:5000/ 或 http://localhost:5000/
(venv) $ flask run
Lab 01
-
製作 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
Lab 01
-
製作 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>
Step 2: Template
- 模板繼承
- 建立基礎模板(base.html) - Bootstrap 4
- 修改繼承基礎模板的模板(index.html)
模板繼承
建立基礎模板(base.html) - Bootstrap 4
- 建立static的資料夾,其下亦分別建立css、js資料夾。
- static與templates資料夾,屬於同一位階。
- css、js資料夾中,分別放入bootstrap.min.css、bootstrap.min.js。
模板繼承
- 至Bootstrap官網下載(css, js)
<!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>
模板繼承
建立基礎模板(base.html) - Bootstrap 4
{% 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 %}
模板繼承
修改繼承基礎模板的模板(index.html)
Lab 02
-
修改 404.html 並套用 base.html
-
修改 500.html 並套用 base.html
Lab 02
-
修改 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
Step 3: Web Form
- 安裝 Flask-WTF 擴充套件
- todolist.py
- 組態設置密鑰(secret key)
- 定義表單類別
- view函式中處理表單
- index.html
- 轉譯表單HTML
安裝 Flask-WTF 擴充套件
(venv) $ pip install flask-wtf
from flask_wtf import FlaskForm
- 使用Flask-WTF 擴充套件 (todolist.py)
組態設置密鑰(secret key)
todolist.py
app.config['SECRET_KEY'] = 'hard to guess string'
- 務必置於app = Flask(__name__)之下
定義表單類別
todolist.py
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
- 請置於匯入的擴充套件之下
view函式中處理表單
todolist.py
@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)
轉譯表單HTML
index.html
<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>
- 轉譯成HTML的表單填寫之欄位
- 放入{% block content %}{% endblock content %}
轉譯表單HTML
index.html
<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>
-
顯示轉譯成HTML的表單欄位之內容
- 放入{% block content %}{% endblock content %}
Step 4: Database
- 安裝資料庫框架的擴充套件
- Flask-SQLAlchemy
- todolist.py
- 設定 Flask-SQLAlchemy
- view函式中處理表單欄位的內容
- 建立 todolist.db
- 用Flask-Migrate進行資料庫遷移
安裝 資料庫框架的擴充套件
(venv) $ pip install Flask-SQLAlchemy
from flask_sqlalchemy import SQLAlchemy
- 匯入Flask-SQLAlchemy 擴充套件 (todolist.py)
Flask-SQLAlchemy
- 物件關係對映(Object-Relational Mapper, ORM)框架
- 它將SQL指令資料庫操作,抽象成物件導向操作。
- 對諸如表、文件此類的資料庫實體,可以簡化成對 Python 物件的操作。
todolist.py
設定 Flask-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)
todolist.py
設定 Flask-SQLAlchemy
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
- 建立Table模型
- 務必置於 db = SQLAlchemy(app) 之下
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
todolist.py
view函式中處理表單欄位的內容
@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)
- view函式
建立 todolist.db
(venv) $ flask shell
>>> from todolist import db
>>> db.drop_all()
>>> db.create_all()
- todolist.db中,亦建立名稱為 TaskDate、TaskItem 的 table
>>> from todolist import TaskDate, TaskItem
>>> TaskDate.query.all()
[]
>>> TaskItem.query.all()
[]
- 已建立 TaskDate、TaskItem 的 table
用Flask-Migrate進行資料庫遷移
from flask_migrate import Migrate
migrate = Migrate(app, db)
(venv) $ pip install Flask-Migrate
- 初始化 (todolist.py)
- 執行初始化指令
(venv) $ flask db init
(venv) $ flask db migrate -m "initial migration"
- 建立遷移腳本
Step 5: Large File Structure
- 大型檔案結構
- 組態設定 (config.py)
- requirements.txt
- app套件
- app主腳本 (todolist.py)
- 單元測試
大型檔案結構
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
# ...
組態設定 (config.py)
- 定義共同的組態設定
大型檔案結構
組態設定 (config.py)
# ...
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.py)
- 註冊各別定義的資料庫組態。
- Development 組態,註冊為預設值。
# ...
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
大型檔案結構
組態設定 (config.py)
- 組態設定 (config.py)
- requirements.txt
- app套件
- app主腳本 (todolist.py)
- 單元測試
大型檔案結構
(venv) $ pip freeze > requirements.txt
requirements.txt
- 紀錄所有的套件依賴項目,以及確切的版本號。
- 透過Windows cmd,自動產生。
大型檔案結構
requirements.txt
- 組態設定 (config.py)
- requirements.txt
- app套件
- app主腳本 (todolist.py)
- 單元測試
大型檔案結構
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
app套件
- 使用App工廠 (app/__init__.py)
- 資料庫模型(app/models.py)
-
藍圖(todolist)
- 藍圖中實作App功能
大型檔案結構
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from config import config #匯入config.py
db = SQLAlchemy()
# ...
app套件 - 使用App工廠(app/__init__.py)
- 匯入Flask擴充套件,且尚未初始化。
在結構化之下,動態套用組態的改變(建立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
app套件 - 使用App工廠(app/__init__.py)
- 建立App工廠函式 create_app(),回傳建立好的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
app套件 - 資料庫模型(app/models.py)
將TaskDate, TaskItem資料表獨立出來。
大型檔案結構
C:\Users\pcsh1\flask_proj
├─app
│ __init__.py
│ models.py --- 資料庫模型
│
├─todolist --- 藍圖:todolist
│ __init__.py
│ forms.py --- 表單物件
│ views.py
│ errors.py
app套件 - 藍圖(todolist)
- 藍圖(blueprint)
- 功能性:每個藍圖,都是被封裝的功能(todolist)。
- 一個flask可以有多個藍圖,且必須註冊才能被顯示。
在結構化之下,定義 路由 & 錯誤頁面處理函式。
大型檔案結構
from flask import Blueprint
#Blueprint('藍圖名稱', 藍圖所在的模組或套件)
todolist = Blueprint('todolist', __name__)
#匯入 路由 & 錯誤頁面處理函式
from . import views, errors
app套件 - 藍圖(todolist) - (app/todolist/__init__.py)
- 建立主藍圖
大型檔案結構
# ...
### 註冊主藍圖(todolist) ###
# 會連接到 app/todolist/__init__.py
from .todolist import todolist as todolist_blueprint
app.register_blueprint(todolist_blueprint)
return app
app套件 - 藍圖(todolist) -
(app/__init__.py)
- 註冊主藍圖
大型檔案結構
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')
app套件 - 藍圖(todolist) -
(app/todolist/forms.py)
- 表單物件
大型檔案結構
from flask import render_template
from . import todolist
from .. import db
from .forms import ToDoListForm #表單物件
from ..models import TaskDate, TaskItem #資料庫模型
# ...
app套件 - 藍圖(todolist) - (app/todolist/views.py)
- 定義 來自藍圖(todolist)的路由
# ...
@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)
大型檔案結構
app套件 - 藍圖(todolist) -
(app/todolist/errors.py)
- 定義 來自藍圖(todolist)的錯誤頁面處理函式
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
大型檔案結構
app套件
- 組態設定 (config.py)
- requirements.txt
- app套件
- app主腳本 (todolist.py)
- 單元測試
大型檔案結構
app主腳本 (todolist.py)
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
大型檔案結構
app主腳本 (todolist.py)
- 組態設定 (config.py)
- requirements.txt
- app套件
- app主腳本 (todolist.py)
- 單元測試
大型檔案結構
單元測試
C:\Users\pcsh1\flask_proj
├─tests
│ test_basics.py
│ __init__.py --- 讓測試目錄成為有效的套件 (空檔案)
大型檔案結構
單元測試 - (tests/test_basics.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)
單元測試 - (todolist.py)
-
透過Windows cmd,執行test1測試
(venv) $ flask test1
大型檔案結構
單元測試
- 組態設定 (config.py)
- requirements.txt
- app套件
- app主腳本 (todolist.py)
- 單元測試
Thanks for listening.
To Do List
By BessyHuang
To Do List
Python Flask - To Do List
- 398