我的Flask網頁開發學習,也是走走停停,三天打魚,兩天晒網。但意外的是,學習與學習之間的空檔,讓我在快要忘記先前所學的技術之際,透過再次複習,不只重拾記憶,更可以深深地加強自己對於這項技術的整體架構與掌握度。
一如先前的預測,未來我的ESG工作,將不再只是單純做資料處理、資料維運、資料科學的任務,而必須進一步加值我們團隊打造的ESG產品,製作成方便客戶使用的「工具」,實踐組織的ESG目標。
目前我自己的網頁開發能力仍不純熟,即便自己對此沒有興趣,但著眼於網頁開發是簡報呈現與工具提供的方式,因此我還是有動機持續學習、應用的。近幾年的目標是希望我能夠將ESG計算工具的產品,先透過Flask做簡易的產品原型Demo,這對於研發而言也是極其重要的。
第二部分、範例:社群部落格app
第八章、使用者身分驗證-密碼雜湊與驗證藍圖(8a、8b)
l 密碼雜湊與驗證藍圖:
git checkout 8a
git checkout 8b
l 所需使用的套件:
Flask-Login-管理已登入的使用者session。
Werkzeug-密碼雜湊化與驗證。
itsdangerous-以加密的方式保護權證的生成與驗證。
l app/models.py:
from werkzeug.security import generate_password_hash, check_password_hash
from . import db
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"))
password_hash =
db.Column(db.String(128))
# @porperty裝飾器將方法定義成屬性,因此它不需要()
@property
def password(self):
raise AttributeError("password is not
a readable attribute")
@password.setter
def password(self, password):
#
generate_password_hash(password, method="pbkdf2:sha256",
salt_length=8),此函式接收純文字密碼,回傳密碼雜湊字串
self.password_hash =
generate_password_hash(password)
def verify_password(self, password):
# check_password_hash(hash,
password),此函式回傳值True代表使用者密碼是正確的
return
check_password_hash(self.password_hash, password)
def __repr__(self):
return "<User
%r>" % self.username
l tests/test_user_model.py:
import unittest
from app import create_app, db
from app.models import User
class UserModelTestCase(unittest.TestCase):
# 在flask shell測試密碼雜湊,u1.password_hash應有值
# u1 = User()
# u1.password = "cat"
def test_password_setter(self):
u =
User(password="cat")
self.assertTrue(u.password_hash
is not None)
# 在flask shell測試密碼雜湊,直接讀取u1.password會造成AttributeError
def test_no_password_getter(self):
u = User(password="cat")
with
self.assertRaises(AttributeError):
u.password
# 在flask shell測試密碼雜湊,輸入正確密碼才可得True
def test_password_verification(self):
u =
User(password="cat")
self.assertTrue(u.verify_password("cat"))
self.assertFalse(u.verify_password("dog"))
# 在flask shell測試密碼雜湊,即便密碼相同,u1.password_hash與u2.password_hash也不相同
# u2 = User()
# u2.password = "cat"
def
test_password_salts_are_random(self):
u =
User(password="cat")
u2 = User(password="cat")
self.assertTrue(u.password_hash
!= u2.password_hash)
l 將使用者身分驗證系統的路由加入為第二個藍圖:
# app/auth/__init__.py
from flask import Blueprint
auth = Blueprint("auth", __name__)
from . import views
# app/auth/views.py
from flask import render_template
from . import auth
@auth.route("/login")
def login():
return
render_template("auth/login.html")
# app/__init__.py
from .auth import auth as auth_blueprint
# url_prefix引數是選用的,當你使用它時,在藍圖裡面定義的所有路由都會被加上前置詞,本例路由會被註冊成/auth/login,且開發web伺服器底下的完整URL會變成http://localhost:5000/auth/login
app.register_blueprint(auth_blueprint, url_prefix="/auth")
第八章、使用者身分驗證-登入登出與使用者註冊(8c、8d)
l 登入登出與使用者註冊:
git checkout 8c
git checkout 8d
l Flask-Login是一種小型但是相當實用的擴充套件,專門用來管理使用者身分驗證系統,且不需要綁定特定的身分驗證機制:
pip install flask-login
l Flask-Login會與app的User物件密切合作,為了能夠使用app的User模型,Flask-Login要求它實作一些常見的屬性與方法:
特性/方法 |
說明 |
is_authenticated |
如果使用者提供有效的登入憑證,它必須是True |
is_active |
如果允許使用者登入,它必須是True |
is_anonymous |
用來代表匿名使用者,它必須是True |
get_id() |
必須回傳使用者獨有的識別碼,編碼成Unicode字串 |
l app/models.py:
from flask_login import UserMixin
from . import login_manager
# 修改User模型來支援使用者登入
class User(UserMixin, db.Model):
__tablename__ = "users"
id = db.Column(db.Integer,
primary_key=True)
email = db.Column(db.String(64),
unique=True, index=True)
username = db.Column(db.String(64),
unique=True, index=True)
role_id = db.Column(db.Integer,
db.ForeignKey("roles.id"))
password_hash =
db.Column(db.String(128))
# 載入使用者的函式,當Flask-Login需要取得已登入的使用者的資訊時就會呼叫它
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
l app/__init__.py:
from flask_login import LoginManager
login_manager = LoginManager()
# login_view屬性的用途是設定登入網頁的端點,因為登入路由在藍圖裡面,它的前面必須加上藍圖名稱
login_manager.login_view = "auth.login"
def create_app(config_name):
# ...
login_manager.init_app(app)
# ...
l app/auth/forms.py:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User
class LoginForm(FlaskForm):
# Email()驗證需要先pip install email_validator
email =
StringField("Email", validators=[DataRequired(), Length(1, 64),
Email()])
password = PasswordField("Password",
validators=[DataRequired()])
remember_me = BooleanField("Keep
me logged in")
submit = SubmitField("Log
In")
class RegistrationForm(FlaskForm):
email =
StringField("Email", validators=[DataRequired(), Length(1, 64),
Email()])
# 0: flags – The regexp flags to use,
for example re.IGNORECASE.
username =
StringField("Username", validators=[DataRequired(), Length(1, 64),
Regexp("^[A-Za-z][A-Za-z0-9_.]*$", 0, "Usernames must have only
letters, numbers, dots or underscores")])
password =
PasswordField("Password", validators=[DataRequired(),
EqualTo("password2", message="Passwords must match.")])
password2 =
PasswordField("Confirm password", validators=[DataRequired()])
submit =
SubmitField("Register")
# 當提交表單時,Flask-WTF將自動驗證表單資料,並呼叫表單類別中定義的驗證方法
def validate_email(self, field):
if
User.query.filter_by(email=field.data.lower()).first():
raise
ValidationError("Email already registered.")
def validate_username(self, field):
if
User.query.filter_by(username=field.data).first():
raise
ValidationError("Username already in use.")
l app/auth/views.py:
from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user, logout_user, login_required
from . import auth
from .. import db
from ..models import User
from .forms import LoginForm, RegistrationForm
@auth.route("/login", methods=["GET", "POST"])
def login():
form = LoginForm()
if form.validate_on_submit():
user =
User.query.filter_by(email=form.email.data.lower()).first()
if user is not None and
user.verify_password(form.password.data):
# login_user()函式來幫使用者session記錄使用者已經登入
# 選用的REMEMBER_COOKIE_DURATION組態選項可用來改變cookie的一年期限預設值
login_user(user,
form.remember_me.data)
# 如果顯示給使用者看的登入表單想要防止使用者在未經授權的情況下前往受保護的URL,Flask-Login就會將那個原始的URL存放在next查詢字串引數;如果沒有next查詢字串引數,就會轉址到首頁
next =
request.args.get("next")
if next is None or not next.startswith("/"):
next =
url_for("main.index")
return redirect(next)
flash("Invalid email or
password.")
return
render_template("auth/login.html", form=form)
@auth.route("/logout")
# 保護路由,只允許通過驗證的使用者造訪它
@login_required
def logout():
logout_user()
flash("You have been logged
out.")
return
redirect(url_for("main.index"))
@auth.route("/register", methods=["GET", "POST"])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user =
User(email=form.email.data.lower(), username=form.username.data, password=form.password.data)
db.session.add(user)
db.session.commit()
flash("You can now
login.")
return
redirect(url_for("auth.login"))
return
render_template('auth/register.html', form=form)
l app/templates/base.html:
<!-- ... -->
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated
%}
<li><a href="{{
url_for('auth.logout') }}">Log Out</a></li>
{% else %}
<li><a href="{{
url_for('auth.login') }}">Log In</a></li>
{% endif %}
</ul>
<!-- ... -->
l app/templates/auth/login.html:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Login{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
<br>
<p>New user? <a
href="{{ url_for('auth.register') }}">Click here to
register</a>.</p>
</div>
{% endblock %}
l app/templates/auth/register.html:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Register{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Register</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
第八章、使用者身分驗證-帳號認證(8e)
l 帳號認證:
git checkout 8e
l 展示itsdangerous如何產生一個含有使用者id的已簽署權杖:
# 在flask shell展示
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
# expires_in引數可設定權杖的到期時間,其單位是秒
s = Serializer(app.config["SECRET_KEY"], expires_in=3600)
token = s.dumps({"confirm": 23})
token à b'eyJhbGciOiJIUzUxMiIsImlhdCI6MTY4Mjc0NTY2MSwiZXhwIjoxNjgyNzQ5MjYxfQ.eyJjb25maXJtIjoyM30.ZBVS3Y-H_ooMCa8cfDrxtZCtcAX00AI0RYIdETcyMpkOPyeIfxcVXF74w3YQUvZNm0XfVckQOWf7jZ8jzwcYTw'
data = s.loads(token)
data à {'confirm': 23}
l app/models.py:
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app
class User(UserMixin, db.Model):
# ...
confirmed = db.Column(db.Boolean,
default=False)
def generate_confirmation_token(self,
expiration=3600):
s =
Serializer(current_app.config["SECRET_KEY"], expiration)
return s.dumps({"confirm": self.id}).decode("utf-8")
def confirm(self, token):
s =
Serializer(current_app.config["SECRET_KEY"])
try:
data =
s.loads(token.encode("utf-8"))
except:
return False
if data.get("confirm")
!= self.id:
return False
self.confirmed = True
db.session.add(self)
return True
l app/auth/views.py:
# ...
# 可以直接利用current_user隨處取得目前的使用者相關資訊(依個人實作User Class設置而定)、登入狀態以及利用裝飾器@login_request驗證登入狀況
from flask_login import login_user, logout_user, login_required, current_user
# ...
# 用before_app_request處理常式來過濾未確認的帳號
@auth.before_app_request
def before_request():
# current_user.is_authenticated已登入
# not current_user.confirmed未啟動(本書範例User Class的confirmed)
# flask透過了endpoint來記錄了跟rule(url)與view_function的關聯,所以有endpoint,就有辦法找到相關的rule與view_function (https://hackmd.io/@shaoeChen/HJiZtEngG/https%3A%2F%2Fhackmd.io%2Fs%2FSyiNhFTvz)
# endpoint不為static,避免相關js、css、icon失效
if current_user.is_authenticated \
and not current_user.confirmed
\
and request.endpoint \
and request.blueprint !=
"auth" \
and request.endpoint !=
"static":
return
redirect(url_for("auth.unconfirmed"))
@auth.route("/unconfirmed")
def unconfirmed():
if current_user.is_anonymous or
current_user.confirmed:
return
redirect(url_for("main.index"))
return
render_template("auth/unconfirmed.html")
# 寄出確認email
@auth.route("/register", methods=["GET", "POST"])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data.lower(),
username=form.username.data, password=form.password.data)
db.session.add(user)
db.session.commit()
token =
user.generate_confirmation_token()
send_email(user.email,
"Confirm Your Account", "auth/email/confirm", user=user,
token=token)
flash("A confirmation email
has been sent to you by email.")
return
redirect(url_for("auth.login"))
return
render_template("auth/register.html", form=form)
# 確認使用者帳號
@auth.route("/confirm/<token>")
@login_required
def confirm(token):
if current_user.confirmed:
return
redirect(url_for("main.index"))
if current_user.confirm(token):
db.session.commit()
flash("You have confirmed
your account. Thanks!")
else:
flash("The confirmation link is invalid or has expired.")
return
redirect(url_for("main.index"))
# 重寄帳號確認email
@auth.route("/confirm")
@login_required
def resend_confirmation():
token =
current_user.generate_confirmation_token()
send_email(current_user.email,
"Confirm Your Account", "auth/email/confirm",
user=current_user, token=token)
flash("A new confirmation email
has been sent to you by email.")
return
redirect(url_for("main.index"))
l app/templates/auth/unconfirmed.html:
<!-- 給未確認的使用者看的網頁 -->
<h1>Hello, {{ current_user.username }}!</h1>
<p>
Need another confirmation email?
<a href="{{
url_for('auth.resend_confirmation') }}">Click here</a>
</p>
l app/templates/auth/email/confirm.txt:
<!-- 確認email的文字內文 -->
Dear {{ user.username }},
To confirm your account please click on the following link:
<!-- 相對URL在網頁背景下可以良好地運作,因為瀏覽器會加上目前網頁的主機名稱與連接埠號碼來將它們轉換成絕對URL,但是用email寄出的URL沒有這種背景,所以我們在url_for()呼叫式傳入_external=True引數,來要求完整的URL -->
{{ url_for("auth.confirm", token=token, _external=True) }}
l tests/test_user_model.py:
# ...
class UserModelTestCase(unittest.TestCase):
# ...
def
test_valid_confirmation_token(self):
u =
User(password="cat")
db.session.add(u)
db.session.commit()
token = u.generate_confirmation_token()
self.assertTrue(u.confirm(token))
def
test_invalid_confirmation_token(self):
u1 =
User(password="cat")
u2 =
User(password="dog")
db.session.add(u1)
db.session.add(u2)
db.session.commit()
token =
u1.generate_confirmation_token()
self.assertFalse(u2.confirm(token))
def
test_expired_confirmation_token(self):
u =
User(password="cat")
db.session.add(u)
db.session.commit()
token =
u.generate_confirmation_token(1)
time.sleep(2)
self.assertFalse(u.confirm(token))
第八章、使用者身分驗證-修改密碼(8f)
l 修改密碼:
git checkout 8f
l app/auth/forms.py:
# ...
class ChangePasswordForm(FlaskForm):
old_password =
PasswordField("Old password", validators=[DataRequired()])
password = PasswordField("New
password", validators=[DataRequired(), EqualTo("password2",
message="Passwords must match.")])
password2 =
PasswordField("Confirm new password", validators=[DataRequired()])
submit = SubmitField("Update
Password")
l app/auth/views.py:
# ...
@auth.route("/change-password", methods=["GET",
"POST"])
@login_required
def change_password():
form = ChangePasswordForm()
if form.validate_on_submit():
if
current_user.verify_password(form.old_password.data):
current_user.password =
form.password.data
db.session.add(current_user)
db.session.commit()
flash("Your password has
been updated.")
return
redirect(url_for("main.index"))
else:
flash("Invalid
password.")
return
render_template("auth/change_password.html", form=form)
l app/templates/base.html:
<!-- ... -->
<ul class="dropdown-menu">
<li><a href="{{
url_for('auth.change_password') }}">Change
Password</a></li>
<li><a href="{{
url_for('auth.logout') }}">Log Out</a></li>
</ul>
<!-- ... -->
l app/templates/auth/change_password.html:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Change Password{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Change Your
Password</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
第八章、使用者身分驗證-重設密碼(8g)
l 重設密碼:
git checkout 8g
l app/models.py:
# ...
class User(UserMixin, db.Model):
# ...
@staticmethod
def reset_password(token,
new_password):
s =
Serializer(current_app.config["SECRET_KEY"])
try:
data = s.loads(token.encode("utf-8"))
except:
return False
user =
User.query.get(data.get("reset"))
if user is None:
return False
user.password = new_password
db.session.add(user)
return True
l app/auth/forms.py:
# ...
class PasswordResetRequestForm(FlaskForm):
email =
StringField("Email", validators=[DataRequired(), Length(1, 64), Email()])
submit = SubmitField("Reset
Password")
class PasswordResetForm(FlaskForm):
password = PasswordField("New
Password", validators=[DataRequired(), EqualTo("password2",
message="Passwords must match")])
password2 =
PasswordField("Confirm password", validators=[DataRequired()])
submit = SubmitField("Reset
Password")
l app/auth/views.py:
# ...
from .forms import PasswordResetRequestForm, PasswordResetForm
# ...
@auth.route("/reset", methods=["GET", "POST"])
def password_reset_request():
if not current_user.is_anonymous:
return
redirect(url_for("main.index"))
form = PasswordResetRequestForm()
if form.validate_on_submit():
user =
User.query.filter_by(email=form.email.data.lower()).first()
if user:
token =
user.generate_reset_token()
send_email(user.email,
"Reset Your Password", "auth/email/reset_password", user=user,
token=token)
flash("An email with
instructions to reset your password has been sent to you.")
return
redirect(url_for("auth.login"))
return
render_template("auth/reset_password.html", form=form)
@auth.route("/reset/<token>", methods=["GET",
"POST"])
def password_reset(token):
if not current_user.is_anonymous:
return
redirect(url_for("main.index"))
form = PasswordResetForm()
if form.validate_on_submit():
if User.reset_password(token,
form.password.data):
db.session.commit()
flash("Your password has
been updated.")
return
redirect(url_for("auth.login"))
else:
return
redirect(url_for("main.index"))
return
render_template("auth/reset_password.html", form=form)
l app/templates/auth/login.html:
<!-- ... -->
<div class="col-md-4">
{{ wtf.quick_form(form) }}
<br>
<p>Forgot your password? <a
href="{{ url_for('auth.password_reset_request') }}">Click here to
reset it</a>.</p>
<p>New user? <a
href="{{ url_for('auth.register') }}">Click here to
register</a>.</p>
</div>
<!-- ... -->
l app/templates/auth/reset_password.html:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Password Reset{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Reset Your
Password</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
l app/templates/auth/email/reset_password.html:
<p>Dear {{ user.username }},</p>
<p>To reset your password <a href="{{
url_for('auth.password_reset', token=token, _external=True) }}">click
here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's
address bar:</p>
<p>{{ url_for('auth.password_reset', token=token, _external=True)
}}</p>
<p>If you have not requested a password reset simply ignore this
message.</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not
monitored.</small></p>
l tests/test_user_model.py:
# ...
class UserModelTestCase(unittest.TestCase):
# ...
def test_valid_reset_token(self):
u =
User(password="cat")
db.session.add(u)
db.session.commit()
token = u.generate_reset_token()
self.assertTrue(User.reset_password(token, "dog"))
self.assertTrue(u.verify_password("dog"))
def test_invalid_reset_token(self):
u =
User(password="cat")
db.session.add(u)
db.session.commit()
token = u.generate_reset_token()
self.assertFalse(User.reset_password(token
+ "a", "horse"))
self.assertTrue(u.verify_password("cat"))
第八章、使用者身分驗證-更改Email地址(8h)
l 更改Email地址:
git checkout 8h
l app/models.py:
# ...
class User(UserMixin, db.Model):
# ...
def generate_email_change_token(self,
new_email, expiration=3600):
s =
Serializer(current_app.config["SECRET_KEY"], expiration)
return s.dumps({"change_email":
self.id, "new_email": new_email}).decode("utf-8")
def change_email(self, token):
s = Serializer(current_app.config["SECRET_KEY"])
try:
data =
s.loads(token.encode("utf-8"))
except:
return False
if data.get("change_email")
!= self.id:
return False
new_email =
data.get("new_email")
if new_email is None:
return False
if
self.query.filter_by(email=new_email).first() is not None:
return False
self.email = new_email
db.session.add(self)
return True
l app/auth/forms.py:
# ...
class ChangeEmailForm(FlaskForm):
email = StringField("New
Email", validators=[DataRequired(), Length(1, 64), Email()])
password =
PasswordField("Password", validators=[DataRequired()])
submit = SubmitField("Update
Email Address")
def validate_email(self, field):
if
User.query.filter_by(email=field.data.lower()).first():
raise ValidationError("Email already
registered.")
l app/auth/views.py:
# ...
from .forms import ChangeEmailForm
# ...
@auth.route("/change_email", methods=["GET",
"POST"])
@login_required
def change_email_request():
form = ChangeEmailForm()
if form.validate_on_submit():
if
current_user.verify_password(form.password.data):
new_email =
form.email.data.lower()
token = current_user.generate_email_change_token(new_email)
send_email(new_email,
"Confirm your email address", "auth/email/change_email", user=current_user,
token=token)
flash("An email with
instructions to confirm your new email address has been sent to you.")
return
redirect(url_for("main.index"))
else:
flash("Invalid email or
password.")
return
render_template("auth/change_email.html", form=form)
@auth.route("/change_email/<token>")
@login_required
def change_email(token):
if current_user.change_email(token):
db.session.commit()
flash("Your email address
has been updated.")
else:
flash("Invalid
request.")
return
redirect(url_for("main.index"))
l app/templates/base.html:
<!-- ... -->
<ul class="dropdown-menu">
<li><a href="{{
url_for('auth.change_password') }}">Change
Password</a></li>
<li><a href="{{
url_for('auth.change_email_request') }}">Change
Email</a></li>
<li><a href="{{
url_for('auth.logout') }}">Log Out</a></li>
</ul>
<!-- ... -->
l app/templates/auth/change_email.html:
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Change Email Address{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Change Your Email
Address</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
l app/templates/auth/email/change_email.html:
<p>Dear {{ user.username }},</p>
<p>To confirm your new email address <a href="{{ url_for('auth.change_email',
token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's
address bar:</p>
<p>{{ url_for('auth.change_email', token=token, _external=True)
}}</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not
monitored.</small></p>
l tests/test_user_model.py:
# ...
class UserModelTestCase(unittest.TestCase):
# ...
def
test_valid_email_change_token(self):
u =
User(email="john@example.com", password="cat")
db.session.add(u)
db.session.commit()
token =
u.generate_email_change_token("susan@example.org")
self.assertTrue(u.change_email(token))
self.assertTrue(u.email ==
"susan@example.org")
def
test_invalid_email_change_token(self):
u1 =
User(email="john@example.com", password="cat")
u2 =
User(email="susan@example.org", password="dog")
db.session.add(u1)
db.session.add(u2)
db.session.commit()
token =
u1.generate_email_change_token("david@example.net")
self.assertFalse(u2.change_email(token))
self.assertTrue(u2.email ==
"susan@example.org")
def
test_duplicate_email_change_token(self):
u1 =
User(email="john@example.com", password="cat")
u2 =
User(email="susan@example.org", password="dog")
db.session.add(u1)
db.session.add(u2)
db.session.commit()
token =
u2.generate_email_change_token("john@example.com")
self.assertFalse(u2.change_email(token))
self.assertTrue(u2.email ==
"susan@example.org")
沒有留言:
張貼留言