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資料夾,其下亦分別建立cssjs資料夾。
    • static與templates資料夾,屬於同一位階。
  • css、js資料夾中,分別放入bootstrap.min.css、bootstrap.min.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