Date: Apr. 28th, 2019
Lecturer: Chia
學習目標:使用Flask app傳送email。
1. 使用pip安裝Flask-Mail擴充套件
(venv) $ pip install flask-mail
os.environ.get('MAIL_USERNAME')
os.environ.get('MAIL_PASSWORD')
sender
recipients
2. 連接外部的SMTP
### 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身份驗證
Google帳號設定網頁 -> 安全性 -> "低安全性應用程式存取權" 開啟!
4. 初始化Flask-Mail
### db_demo.py ###
from flask_mail import Mail
mail = Mail(app)
5. Windows cmd 設定環境變數
(venv) $ set MAIL_USERNAME=pcsh110576@gmail.com
(venv) $ set MAIL_PASSWORD=**************
(venv) $ set FLASK_APP=db_demo.py
(venv) $ flask shell
>>> from flask_mail import Message
>>> from db_demo import mail
>>> msg = Message('test_email', sender='pcsh110576@gmail.com',
recipients=['404040523@mail.fju.edu.tw'])
>>> msg.html = 'This is the <b>flask test</b> mail.'
>>> with app.app_context():
... mail.send(msg)
>>>
>>> msg.body = 'This is the plain text body'
from flask_mail import Message
#主旨的開頭文字
app.config['MAIL_SUBJECT_PREFIX'] = '[Flasky]'
#寄件者的地址,與SMTP Server相同帳號
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)
mail.send(msg)
msg.html = render_template(template + '.html', **kwargs)
os.environ.get('MAIL_USERNAME')
os.environ.get('MAIL_PASSWORD')
app.config['MAIL_SENDER']
app.config['MAIL_ADMIN']
建立位置:templates/mail/new_user.txt
User {{ user.username }} has joined. (txt)
建立位置:templates/mail/new_user.html
User <b>{{ user.username }}</b> has joined. (html)
### db_demo.py ###
# ...
#收件者
app.config['MAIL_ADMIN'] = os.environ.get('FLASKY_ADMIN')
@app.route('/name_form', methods=['GET', 'POST'])
def name_form():
# ...
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
# ...
(venv) $ set MAIL_USERNAME=pcsh110576@gmail.com
(venv) $ set MAIL_PASSWORD=**************
(venv) $ set FLASKY_ADMIN=404040523@mail.fju.edu.tw
先建立HTML模板,再設定環境變數(MAIL_USERNAME、MAIL_PASSWORD、FLASKY_ADMIN),最終寄出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)
Date: May. 12th, 2019
Lecturer: Chia
C:\Users\pcsh1\flask_proj
│ config.py --- 組態設定
│ db_demo.py --- 定義Flask App實例 & 協助管理App的工作
│ requirements.txt --- 套件依賴項目,以便重建一致的虛擬環境
│
├─app --- (app套件) Flask App
├─migrations --- 資料庫遷移腳本
├─tests --- (tests套件) 單元測試
└─venv --- Python虛擬環境
import os
basedir = os.path.abspath(os.path.dirname(__file__))
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
@staticmethod #實作空的init_app()方法
def init_app(app):
pass
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'mysql://flaskdb_chia:gibe258deny700@localhost:3306/flaskdb'
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 #將Development組態,註冊為預設值
}
C:\Users\pcsh1\flask_proj
├─app
│ email.py --- email支援函式
│ 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
Useful during testing, where the configuration can often be different from development or production.
The steps of initialize a Flask project :
These steps can be put together in a single function called an Application Factory function.
### 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()
To start, import all of the modules (including the Flask extensions) that are needed for the Flask project:
Next, create the instances(實例) of each of the Flask extensions without initializing them:
#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
Next, the actual Application Factory function (create_app) is defined:
App工廠的轉化工作,引出了路由的複雜化...
Allow you to cleanly organize the source code of your Flask application.
### app/main/__init__.py ###
from flask import Blueprint
#實例化Blueprint類別。
#此類別的建構式必須接收兩個引數:藍圖名稱與藍圖所在的模組或套件。
main = Blueprint('main', __name__)
#匯入app套件的模組,以建立與藍圖的關係 (包含app路由&錯誤頁面處理函式)
# . 代表目前的套件; .. 代表目前套件的父代
from . import views, errors #相對匯入
### app/__init__.py ###
# ...
### 註冊主藍圖 ###
#連接到app/main/__init.py
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
### app/main/errors.py ###
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
from flask import render_template, session, redirect, url_for, flash, current_app
from datetime import datetime
from . import main
from .forms import NameForm #表單物件
from .. import db
from ..models import User
from ..email import send_email
### app/main/views.py ###
@main.route('/name_form', methods=['GET', 'POST']) #路由裝飾器來自藍圖(main.route)
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))
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/main/forms.py ###
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)
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
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)
### 單元測試 ###
(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
C:\Users\pcsh1\flask_beginner_1.2.3.4
├─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') #動態套用testing組態設定
self.app_context = self.app.app_context() #維持目前(套用testingDB)的狀態
self.app_context.push() #push() is used to add items to the stack.
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)
#verbosity=1 輸出結果之詳細程度(簡略) vs. verbosity=2 (詳細)
(venv) $ flask test1
C:\Users\pcsh1\flask_beginner_1.2.3.4
├─migrations
│ versions
│ ...
(venv) $ pip install Flask-Migrate
安裝套件
初始化(db_demo.py)
from flask_migrate import Migrate
##初始化Flask-Migrate
migrate = Migrate(app, db)
執行指令
(venv) $ flask db init
testcol = db.Column(db.Integer) #新增至models.py
建立遷移腳本
執行遷移腳本
(venv) $ flask db migrate
(venv) $ flask db upgrade
先修改完model.py之後,使用以下指令。
檢查是否更新到資料庫
show databases;
use flaskdb;
show tables;
SELECT * FROM users;
(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