--- title: Flask - Email, Large app structure tags: python, flask, Web Form, Email, large app structure --- > [TOC] > > Reference Website: > 1. [smtp交易指令教學,可以用來測試smtp郵件伺服器是否正常](http://www.dreamjtech.com/content/smtp%E4%BA%A4%E6%98%93%E6%8C%87%E4%BB%A4%E6%95%99%E5%AD%B8%E5%8F%AF%E4%BB%A5%E7%94%A8%E4%BE%86%E6%B8%AC%E8%A9%A6smtp%E9%83%B5%E4%BB%B6%E4%BC%BA%E6%9C%8D%E5%99%A8%E6%98%AF%E5%90%A6%E6%AD%A3%E5%B8%B8) > 2. [SMTP 實例](https://ithelp.ithome.com.tw/articles/10189886) > 3. [SMTP 協定](http://www.tsnien.idv.tw/Internet_WebBook/chap14/14-4%20SMTP%20%E5%8D%94%E5%AE%9A.html) > 4. [Structuring a Flask Project - Blueprints](https://www.patricksoftwareblog.com/structuring-a-flask-project/) > 5. [A Better Application Structure](https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xv-a-better-application-structure) --- # ***Flask - Email*** * 學習目標:使用Flask app傳送email。 * 套件的選擇:(二擇一) 1. [Python標準函式庫的`smtplib套件`](https://docs.python.org/3/library/smtplib.html) 2. `Flask-Mail`擴充套件 (已包含smtplib,且與Flask做很好的整合) :::info * ==SMTP 簡易郵件傳輸通訊協定== (Simple Mail Transfer Protocol) * 用於從"來源地址"到"目的地址"傳輸郵件的規範,通過它來控制郵件的中轉方式。 * 幫助每台電腦在發送或中轉信件時找到下一個目的地。 * SMTP認證的目的: * 避免用戶受到垃圾郵件的侵擾。 * ==SMTP伺服器==,就是遵循SMTP協定的發送郵件伺服器。 ::: ## **用Flask-Mail來支援email** 1. 使用`pip`安裝`Flask-Mail`擴充套件 ``` (venv) $ pip install flask-mail ``` * 此擴充套件會連接簡易郵件傳輸通訊協定(SMTP)伺服器,並把欲寄出的郵件送給它。 * Flask-Mail SMTP伺服器組態鍵: | 組態鍵 | 預設值 | 說明 | | -------- | -------- | -------- | | MAIL_SERVER | localhost | email伺服器的主機名稱或IP位址 | | MAIL_PORT | 25 | email伺服器的連接埠 | | MAIL_USE_TLS | False | 啟用傳輸層安全性(TLS) | | MAIL_USE_SSL | False | 啟用安全通訊端層(SSL) | | MAIL_USERNAME | None | 郵件帳號的使用者名稱 | | MAIL_PASSWORD | None | 郵件帳號的密碼 | * 預設:無身分驗證的情況下,送出郵件。 2. 連接外部的SMTP(使用Gmail帳號傳送email) * 為了保護帳號資訊,故不把帳號資訊直接寫在腳本裡,而是從環境變數匯入。 ```python= ### db_demo.py:設置Flask-Mail來使用Gmail ### import os # ... app.config['MAIL_SERVER'] = 'smtp.googlemail.com' app.config['MAIL_PORT'] = 587 app.config['MAIL_USE_TLS'] = True app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME') app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD') ``` 3. 設定Gmail以接受SMTP身份驗證 * 基於安全理由,Gmail帳號會要求外部app使用OAuth2驗證,來連接email伺服器。然而,Python的smtp程式庫並不支援。 * 若要讓Gmail帳號接受標準的SMTP身份驗證,設定如下: * [Google帳號設定網頁](https://myaccount.google.com/) -> 安全性 -> 開啟! ![](https://i.imgur.com/5CQ7rOm.png) 4. 初始化Flask-Mail ```python= ### db_demo.py ### from flask_mail import Mail mail = Mail(app) ``` 5. Windows設定環境變數 * 匯入email伺服器`使用者名稱與密碼`的環境變數 ``` (venv) $ set MAIL_USERNAME=pcsh110576@gmail.com (venv) $ set MAIL_PASSWORD=************** ``` * 若是Linux或macOS: ``` (venv) $ export MAIL_USERNAME=pcsh110576@gmail.com (venv) $ export MAIL_PASSWORD=************** ``` ### ***從Python Shell寄送email*** * 目的:透過Python Shell測試設置的結果。 * 做法:必須每次手動建立email訊息。 * 郵件內容的呈現方式:(二擇一) * `msg.body` 純文字 * `msg.html` HTML ```python= (venv) $ set FLASK_APP=db_demo.py (venv) $ flask shell >>> from flask_mail import Message >>> from db_demo import mail >>> msg = Message('test_email 0.0', sender='pcsh110576@mail.fju.edu.tw', recipients=['404040523@mail.fju.edu.tw']) >>> msg.body = 'This is the plain text body' >>> msg.html = 'This is the <b>flask test</b> mail. ===' >>> with app.app_context(): ... mail.send(msg) ... >>> ``` :::success ### 資料庫 (上次增加的部分) ``` pip install flask-sqlalchemy pip install flask-migrate ``` ```python= from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://flaskdb_chia:gibe258deny700@localhost:3306/flaskdb' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) migrate = Migrate(app, db) class Role(db.Model): __tablename__ = 'roles' id = db.Column(db.Integer, primary_key = True) name = db.Column(db.String(64), unique = True) users = db.relationship('User', backref='role', lazy='dynamic') def __repr__(self): return '<Role %r>' % self.name class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key = True) username = db.Column(db.String(64), unique = True, index = True) role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) def __repr__(self): return '<User %r>' % self.username @app.route('/name_form', methods=['GET', 'POST']) def name_form(): form = NameForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.name.data).first() if user is None: user = User(username=form.name.data) db.session.add(user) db.session.commit() session['known'] = False else: session['known'] = True session['name'] = form.name.data form.name.data = '' return redirect(url_for('name_form')) return render_template('webform.html', form=form, name=session.get('name'), known=session.get('known', False)) ``` ::: ### ***整合email與app*** * 目的:避免每次手動建立email訊息,故將email寄送程式寫成一個函式。 * 優點:彈性高,且可將Jinja2模板轉譯成email內文。 ```python= ### db_demo.py:支援email ### from flask_mail import Message #主旨的開頭文字 app.config['MAIL_SUBJECT_PREFIX'] = '[Flasky]' #寄件者的地址,同欲驗證的gmail帳號 app.config['MAIL_SENDER'] = 'Flasky Admin <pcsh110576@gmail.com>' #send_email(收件人地址, 主旨, email內文模板, 關鍵字引數) def send_email(to, subject, template, **kwargs): msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + subject, sender = app.config['MAIL_SENDER'], recipients=[to]) #msg.body = render_template(template + '.txt', **kwargs) msg.html = render_template(template + '.html', **kwargs) mail.send(msg) ``` * 建立email內文模板 1. 純文字模板`templates/mail/new_user.txt` ```htmlmixed= User {{ user.username }} has joined. (txt) ``` 2. HTML模板`templates/mail/new_user.html` ```htmlmixed= User <b>{{ user.username }}</b> has joined. (html) ``` > `關鍵字引數`會被送給render_template(),讓email內文模板將他們當成模板變數使用。(亦即`{{ user.username }}`) * 擴充`db_demo.py`name_form()的view函式 * 當收到表單送來的新名字時,寄一封email給管理者。 ```python= ### db_demo.py ### # ... #收件者 app.config['MAIL_ADMIN'] = os.environ.get('FLASKY_ADMIN') # ... @app.route('/name_form', methods=['GET', 'POST']) def name_form(): form = NameForm() if form.validate_on_submit(): user = User.query.filter_by(username=form.name.data).first() if user is None: user = User(username=form.name.data) db.session.add(user) db.session.commit() #表單的輸入會寫進database session['known'] = False ### 新增 if app.config['MAIL_ADMIN']: send_email(app.config['MAIL_ADMIN'], 'New User', 'mail/new_user', user=user) else: session['known'] = True session['name'] = form.name.data form.name.data = '' return redirect(url_for('name_form')) return render_template('webform.html', form=form, name=session.get('name'), known=session.get('known', False)) ``` * Windows設定環境變數 ``` (venv) $ set MAIL_USERNAME=pcsh110576@gmail.com (venv) $ set MAIL_PASSWORD=************** (venv) $ set FLASKY_ADMIN=bessy110576@gmail.com ``` * 若是Linux或macOS: ``` (venv) $ export MAIL_USERNAME=pcsh110576@gmail.com (venv) $ export MAIL_PASSWORD=************** (venv) $ export FLASKY_ADMIN=bessy110576@gmail.com ``` ![](https://i.imgur.com/WLezRR5.png) * 確認資料有無寫入資料庫 ``` > mysql -u flaskdb_chia -p MariaDB [(none)]> show databases; MariaDB [(none)]> use flaskdb; MariaDB [flaskdb]> SELECT * FROM users; ``` ### ***寄送非同步email*** * 目的:避免處理請求時產生延遲,故將email寄送函式移往背景執行緒。 > [Flask的Context機制](https://blog.tonyseek.com/post/the-context-mechanism-of-flask/) > [flask學習筆記 -- 上下文環境與執行緒隔離](https://www.itread01.com/content/1548687065.html) * 切記:若app需要寄送大量email,應該使用專門的工作來寄送,而非寄出每一封信時就啟動一個新的執行緒。 * 可將send_async_email()函式的執行工作送到 [Celery](http://www.celeryproject.org/) 工作佇列。 ```python= ### db_demo.py:支援非同步email ### #當你要同時做很多事情時,就可以用到threading達成多執行緒。 from threading import Thread def send_email(to, subject, template, **kwargs): msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + subject, sender = app.config['MAIL_SENDER'], recipients=[to]) msg.body = render_template(template + '.txt', **kwargs) #msg.html = render_template(template + '.html', **kwargs) thr = Thread(target=send_async_email, args=[app, msg]) thr.start() return thr def send_async_email(app, msg): with app.app_context(): mail.send(msg) ``` --- # ***Flask - Large App Structure*** * 原因:`db_demo.py`腳本變大,不便使用。 * 學習目標:架構較大型的App,以增加擴充性。 * Flask VS. 多數其他web框架 * Flask不會限制大型專案的組織架構。(完全由開發者主導) :::warning ## App基本結構 ``` (venv) $ tree /f > tree.txt C:\Users\pcsh1\flask_proj │ config.py --- 組態設定 │ db_demo.py --- 定義Flask App實例 & 協助管理App的工作 │ requirements.txt --- 套件依賴項目,以便重建一致的虛擬環境 │ ├─app --- (app套件) Flask App ├─migrations --- 資料庫遷移腳本 ├─tests --- (tests套件) 單元測試 └─venv --- Python虛擬環境 ``` ::: ## 1. 組態設定選項`config.py` * app通常需要多組的組態設定。 > email伺服器的組態:從環境變數匯入,預設指向Gmail伺服器。 > 通常在開發期間,使用預設值。但是,在產品伺服器上,應設置對應的環境變數。 * 分離`Development(開發)`、`Testing(測試)`與`Production(產品)`期間所使用的不同資料庫,以免相互干擾。 * 簡單的字典式組態:`app.config[''] = ''` -> 改為【組態類別階層】 > [flask web 開發中 config 文件中 init_app 函數的作用](https://blog.csdn.net/a447685024/article/details/52254134) ```python= import os basedir = os.path.abspath(os.path.dirname(__file__)) #Config基礎類別:所有組態共同的設定 class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' #可以從環境變數匯入,或是使用預設值 MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com') MAIL_PORT = int(os.environ.get('MAIL_PORT', '587')) MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1'] MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') MAIL_SUBJECT_PREFIX = '[Flasky]' MAIL_SENDER = 'Flasky Admin <pcsh110576@gmail.com>' MAIL_ADMIN = os.environ.get('FLASKY_ADMIN') SQLALCHEMY_TRACK_MODIFICATIONS = False #實作空的init_app()方法 @staticmethod def init_app(app): pass #子類別:分別定義特定組態專屬的設定,讓app在各個組態設置中使用不同的資料庫 class DevelopmentConfig(Config): DEBUG = True SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \ 'mysql://flaskdb_chia:gibe258deny700@localhost:3306/flaskdb' #每一個組態都會試著從環境變數匯入資料庫URL。若無法匯入,則使用預設的資料庫。 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字典 config = { 'development': DevelopmentConfig, 'testing': TestingConfig, 'production': ProductionConfig, 'default': DevelopmentConfig #將Development組態,註冊為預設值 } ``` ## 2. App套件 :::warning * App套件內存放: * app 的 所有程式碼 * `templates` 模板 * `static` 靜態檔案 * `models.py` 資料庫模型 * `email.py` email支援函式 ``` C:\Users\pcsh1\flask_proj ├─app │ email.py │ models.py │ __init__.py │ ├─main │ │ errors.py │ │ forms.py │ │ views.py │ │ __init__.py │ ├─static ├─templates │ 404.html │ 500.html │ base.html │ base_index.html │ webform.html │ └─mail │ new_user.html │ new_user.txt ``` ::: ### I. ==使用App工廠==:`app/__init__.py` * 用單一檔案來建立App,等於是在全域範圍內建立App,固然方便,但無法動態套用組態的改變。 * 當腳本開始執行時,App實例就被建立,此時進行組態的改變已經太晚了。這一點對於「單元測試」特別重要! * `App套件建構式`的寫法: * 目的: * 讓腳本有時間設定組態 (動態套用組態的改變) * 亦可建立多個App實例 * 作法: * 透過延遲App的建立,將它移至工廠函式內,再利用腳本呼叫它。 ```python= ### app/__init__.py:App套件建構式 ### ##匯入多數目前正在使用的Flask擴充套件 from flask import Flask from flask_bootstrap import Bootstrap from flask_mail import Mail from flask_moment import Moment from flask_sqlalchemy import SQLAlchemy #匯入config.py from config import config #尚未初始化 bootstrap = Bootstrap() db = SQLAlchemy() mail = Mail() moment = Moment() #create_app()為"App工廠函式",回傳建立好的App實例。 def create_app(config_name): #config_name接收組態名稱,以讓App使用 #建立App實例 app = Flask(__name__) #將config.py內定義的組態類別,其所儲存的組態設定直接匯入App app.config.from_object(config[config_name]) #App初始化,使用init_app() config[config_name].init_app(app) bootstrap.init_app(app) db.init_app(app) mail.init_app(app) moment.init_app(app) ### 註冊主藍圖 ### # 在這裡指派"路由"與"錯誤頁面處理函式" # # 直到藍圖被app註冊時,才成為app的一部分。 return app ``` ### II. ==在藍圖中實作App功能== * 單一腳本app: * App實例位於全域範圍內,故能用`app.route裝飾器`來定義路由。 * 但是,現在App是在執行期建立的,`app.route裝飾器`在create_app()執行後才存在,此時為時已晚。 * 自訂錯誤頁面處理函式,由`app.errorhandler裝飾器`所定義,亦面臨相同問題。 * `藍圖(blueprint)`的寫法: * 與App的相似之處:可以定義路由和錯誤處理函式。 * 與App的差異之處:當在藍圖裡定義時,處於休眠狀態。直到藍圖被app註冊時,才成為app的一部分。 * 方式:(二擇一) 1. 使用單一檔案來定義所有的藍圖。 2. 使用結構化的方式。(高彈性) * 在一個app套件內,用多個模組建立他們。 * 做法: * 藉由實例化Blueprint類別來建立的。 #### 1. ==**建立主藍圖**== `app/main/__init__.py` > [Flask Blueprint 藍圖](https://blog.csdn.net/jmilk/article/details/53342517) > 目標:在app套件內,建立子套件來乘載app的第一個藍圖。 > 特別留意: > * 最後才將`app/main/__init__.py`腳本匯入模組,以避免循環的依賴關係造成錯誤。 > * main必須先被定義,`views` `errors`才能匯入main藍圖物件,否則將會匯入失敗。 ```python= from flask import Blueprint #實例化Blueprint類別。 #此類別的建構式必須接收兩個引數:藍圖名稱與藍圖所在的模組或套件。 main = Blueprint('main', __name__) #匯入app套件的模組,以建立與藍圖的關係 (包含app路由&錯誤頁面處理函式) # . 代表目前的套件; .. 代表目前套件的父代 from . import views, errors #相對匯入 ``` #### 2. ==**註冊主藍圖**== `app/__init__.py` > 藍圖是使用`create_app()工廠函式`內的app來註冊。 ```python= # ... ### 註冊主藍圖 ### #連接到app/main/__init.py from .main import main as main_blueprint app.register_blueprint(main_blueprint) return app ``` #### 3. ==**主藍圖裡的錯誤處理函式**== `app/main/errors.py` > * 當使用`errorhandler裝飾器`時,處理函式只會在"錯誤是在藍圖定義的路由中發生"時執行。 > * 若要設定遍及app範圍的錯誤處理函式,則須改用`app_errorhandler裝飾器`。 ```python= from flask import render_template from . import main # 全域(app_errorhandler裝飾器)的錯誤處理函式 # @main.app_errorhandler(404) #路由裝飾器來自藍圖(main.route) def page_not_found(e): return render_template('404.html'), 404 @main.app_errorhandler(500) def internal_server_error(e): return render_template('500.html'), 500 ``` #### 4. ==**主藍圖裡的app路由**== `app/main/views.py` > 在藍圖裡編寫view函式,差異有二: > 1. 路由裝飾器來自藍圖,故使用`main.route`,而非`app.route` > 2. `url_for()`函式的用法: > * 第一個引數(路由的端點名稱),從預設的view函式名稱`index`,改為套用`藍圖名稱(main)`的`main.index`。 > * Flask會將`命名空間(藍圖的名稱)`套用到在藍圖中定義的所有端點。 > 各個藍圖可使用相同的端點名稱來定義view函式,且不會造成衝突。 ```python= from flask import render_template, session, redirect, url_for from datetime import datetime from flask import flash from . import main from .forms import NameForm #表單物件 from .. import db from ..models import User from ..email import send_email from flask import current_app #路由裝飾器來自藍圖(main.route) @main.route('/name_form', methods=['GET', 'POST']) def name_form(): form = NameForm() if form.validate_on_submit(): # 用表單收到的name在資料庫中查詢 user = User.query.filter_by(username=form.name.data).first() # 查無此姓名的話,將該姓名寫入資料庫 if user is None: user = User(username=form.name.data) db.session.add(user) db.session.commit() #把表單所輸入的,寫進資料庫 session['known'] = False if current_app.config['MAIL_ADMIN']: send_email(current_app.config['MAIL_ADMIN'], 'New User', 'mail/new_user', user=user) ### else: session['known'] = True session['name'] = form.name.data form.name.data = '' old_name = session.get('name') if old_name is not None and old_name != form.name.data: flash('Looks like you have changed your name!') # 將命名空間套用到在藍圖中所定義的所有端點 return redirect(url_for('main.name_form')) #等同於 '.name_form' return render_template('webform.html', form=form, name=session.get('name'), current_time=datetime.utcnow(), known=session.get('known', False)) ``` #### 5. ==**表單物件**== `app/main/forms.py` ```python= from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import DataRequired class NameForm(FlaskForm): name = StringField('What is your name?', validators=[DataRequired()]) submit = SubmitField('Submit') ``` ### `app/email.py` ```python= from threading import Thread from flask import render_template, current_app ### from flask_mail import Message from . import mail def send_email(to, subject, template, **kwargs): app = current_app._get_current_object() ### msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + subject, sender = app.config['MAIL_SENDER'], recipients=[to]) msg.body = render_template(template + '.txt', **kwargs) msg.html = render_template(template + '.html', **kwargs) thr = Thread(target=send_async_email, args=[app, msg]) thr.start() return thr def send_async_email(app, msg): with app.app_context(): mail.send(msg) ``` ### `app/models.py` ```python= from . import db class Role(db.Model): __tablename__ = 'roles' id = db.Column(db.Integer, primary_key = True) name = db.Column(db.String(64), unique = True) users = db.relationship('User', backref='role', lazy='dynamic') def __repr__(self): return '<Role %r>' % self.name class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key = True) username = db.Column(db.String(64), unique = True, index = True) role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) def __repr__(self): return '<User %r>' % self.username ``` ## 3. App主腳本 `db_demo.py` * 定義App實例 ```python= # 主腳本 # import os from app import create_app, db from app.models import User, Role from flask_migrate import Migrate #建立一個App,從環境變數或預設來取得組態設置。 app = create_app(os.getenv('FLASK_CONFIG') or 'default') ##初始化Flask-Migrate migrate = Migrate(app, db) ##Python殼層的自訂context @app.shell_context_processor def make_shell_context(): return dict(db=db, User=User, Role=Role) ### 單元測試 ### ``` * Windows設定環境變數 (讓flask命令找到App實例) ``` (venv) $ set FLASK_APP=db_demo.py (venv) $ set FLASK_DEBUG=1 ``` * 若是Linux或macOS: ``` (venv) $ export FLASK_APP=db_demo.py (venv) $ export FLASK_DEBUG=1 ``` ## 4. requirements檔 > 紀錄所有的套件依賴項目,以及確切的版本號。 * 透過Windows cmd,自動產生requirements.txt ``` (venv) $ pip freeze > requirements.txt ``` * 複製完全一樣的虛擬環境 ``` (venv) $ deactivate $ cd .. $ mkdir test $ cd test $ virtualenv venv_test $ venv_test\Scripts\activate (venv_test) $ pip install C:\Users\pcsh1\OneDrive\桌面\mysqlclient-1.4.2-cp37-cp37m-win32.whl (venv_test) $ pip install -r requirements.txt (venv_test) $ pip freeze ``` ## 5. 單元測試 :::warning * 建立`tests`資料夾和`tests/__init__.py` ``` C:\Users\pcsh1\flask_beginner_1.2.3.4 ├─tests │ test_basics.py │ __init__.py --- 讓測試目錄成為有效的套件 (空檔案) ``` ::: ### I. ==單元測試== `tests/test_basics.py` * 使用Python標準程式庫的[unittest套件](https://docs.python.org/3/library/unittest.html) * `__init__.py` 可以為空檔案,因為`unittest套件`會掃描所有的模組來尋找測試程式。 * 目標: * 定義兩個簡單的測試(`test_app_exists`、`test_app_is_testing`) ```python= 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() # 開頭為"test_"的方法,是我們要執行的測試 # #確認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']) ``` ### II. 單元測試啟動命令 `db_demo.py` > [python腳本中verbosity是什麼意思?](https://zhidao.baidu.com/question/306632744408746084.html) ```python= # ... #自訂命令 @app.cli.command() def test1(): """Run the unit tests.""" import unittest tests = unittest.TestLoader().discover('tests') unittest.TextTestRunner(verbosity=2).run(tests) ``` * 透過Windows cmd,執行test1測試 ``` (venv) $ flask test1 ``` ## 6. 設定資料庫 > [搞懂Flask-Migrate](https://ithelp.ithome.com.tw/articles/10205751?sc=iThelpR) > [Flask-Migrate](https://blog.burn-i.com/20180418/flask-migrate/) * App會先從環境變數取得資料庫URL,若找不到則使用預設的資料庫。 * 透過Windows cmd,當Model的結構有異動時自動更新資料庫。 ``` (venv) $ flask db upgrade ``` ## 7. 執行App * 透過Windows cmd,啟動App腳本 & 匯入環境變數 ``` (venv) $ set FLASK_APP=db_demo.py (venv) $ set FLASK_DEBUG=1 (venv) $ set MAIL_USERNAME=pcsh110576@gmail.com (venv) $ set MAIL_PASSWORD=************** (venv) $ set FLASKY_ADMIN=bessy110576@gmail.com (venv) $ flask run ```