Flask[4]

Review

一樣是快一個月前的東西

Session

  • flask內建的 = 簽過名的cookie
  • 具體操作方式像dict
  • 相較純cookie安全,通常拿來存一些個人資料、身分驗證等不能給使用者改的東東
#加密用的secret key
app.config['SECRET_KEY'] = 'c4dffa417abe4d31936cdf52d3a6d7ae'

# 存數據
session['key'] = 'value' 

# 取數據
val = session.get('key')

.env

  • 存一些不能公開的東東
  • 用變數存
secret_code = 6767
password = "password"
from flask import Flask
 
import os
from dotenv import load_dotenv
load_dotenv()
 
app = Flask(__name__)
 
@app.route("/")
def main():
    pw = os.getenv("password")
    return f'password is: {pw}'
 
if __name__ == '__main__':
    app.run(debug=True,port=5000)

對不起例子偷之前的

Database

Database的簡稱是DB

還記得嗎

from flask import Flask
from flask import redirect
from flask import request, render_template, url_for, session

app = Flask(__name__)

secret_key = 'c4dffa417abe4d31936cdf52d3a6d7ae'
app.config['SECRET_KEY'] = secret_key #用來加密的

@app.route("/")
def main():
    return render_template("index.html")

@app.route("/post_example", methods=['GET', 'POST']) #form把資訊傳到 "/post_example", 我們和flask說這是post method
def post_example():
    if request.method == 'POST':
        #取得我們form的資料

        #把我們input的資料存到session
        session['name'] = request.form['username']
        session['password'] = request.form['password']

        #判斷我們的輸入是否正確
        if session['name'] == 'admin' and session['password'] == 'admin':
            return redirect(url_for('success'))
        else:
            return redirect(url_for('fail'))
    else:
        return redirect("http://127.0.0.1:5000/") #如果沒收到,就回到主畫面
    
@app.route("/success")
def success():
    #這裡用session,表示可以在路由之間傳送資訊
    return f"success! your name is {session['name']}, and your password is {session['password']}"

@app.route("/fail")
def fail():
    return f"login failed, please try again"

if __name__ == '__main__':
    app.run(debug=True,port=5000)

這邊是寫死的,如果有DB就可以動態查詢

正式簡介

  • 資料庫
  • 顧名思義,用於儲存資料
  • 基本操作:新增、查詢、刪除、修改...
  • 分為SQL型和NoSQL型

SQL v.s. NoSQL

SQLNoSQL
中譯名稱關聯式資料庫非關聯式資料庫
格式資料表格,欄&列很多種 e.g. JSON、圖形
查詢方式標準SQL語法依使用的種類而定
資料關聯型
彈性低(定了就定了)
例子MySQL、PostgreSQL、SQLiteMongoDB、Redis

Flask-SQLAlchemy

&

SQL資料庫(SQLite)操作

先補一下SQLAlchemy

  • Python ORM框架
  • ORM
    • 以物件導向方式操作資料庫
    • 將資料庫的表格與欄位自動對應(映射)為程式碼中的物件與屬性
    • 優:省去學不同資料庫之間的 SQL 語法、防SQL injection
    • 缺:效能變差

Flask-SQLAchemy

  • only for flask
  • 寫法跟SQLAlchemy不盡相同
  • 簡化配置、自動化管理資料庫連線
pip install Flask-SQLAlchemy
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///site.db"

db = SQLAlchemy(app)

Setup

設定路徑(注意是URI)

初始化物件

資料模型

  • 定義資料表的結構
  • 實踐方式:寫一個class繼承db.Model
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String

class User(db.Model):
    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
    email: Mapped[str]
    password: Mapped[str] = mapped_column(String(15), nullable=False)

New(3.1.x之後)

使用與sqlalchemy相近的db.mapped_column()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String)
    password = db.Column(db.String(15), nullable=False)

Old

使用db.column(),裡面存資料型別、條件等等

EZ DB Setup

Step1. 開新terminal

Step2. 打python

EZ DB Setup

Step3. 打這些(如果檔名跟變數名稱不一樣記得改)

Step4. 如果你前面都沒報錯,然後有一個叫instance的資料夾裡有一個叫site.db的檔案,那恭喜你成功創好DB了:)

EZ DB Setup

可以看到表也建起來了

EZ DB Setup

對路徑有更詳細的管理(with os)

基本操作

新增資料

from app import User

with app.app_context():
	user = User(username="Roger", email="rogeris2486@gmail.com", password="2486")
	db.session.add(user)
	db.session.commit()

創一個user object

然後用db.session.add() &  db.session.commit()

查詢資料

with app.app_context():
	User.query.all() #old
    
with app.app_context():
	db.session.scalars(db.select(User)).all() #new

query.all()可以看所有的(會是一個list)

可以加上filter_by()來定查詢條件

*.first() 會回傳找到的第一個

with app.app_context():
	user = User.query.filter_by(username='Roger').first()
    print(user.email) #old
    
with app.app_context():
	user = db.session.execute(db.select(User).filter_by(username="Roger")).scalar_one()
	print(user.email) #new

更新資料

直接改query到的然後commit就行

with app.app_context():    
	user_to_change = User.query.filter_by(username="Roger").first()
	user_to_change.password = "6767" 
	db.session.commit() #update的部分一樣的

刪除資料

same

with app.app_context():    
	user_to_delete = User.query.filter_by(username="Roger").first()
	db.session.delete(user_to_delete)
	db.session.commit()
with app.app_context():
	db.drop_all()

drop_all()可以刪掉所有table

補充:__repr__

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String)
    password = db.Column(db.String(15), nullable=False)

    def __repr__(self):
        return f'User(username="{self.username}", email="{self.email}", password="{self.password}")'
  • 「定義物件的官方字串表示形式」
  • debug用,能比較清楚知道報錯內容

before

after

資料表關聯

資料表關聯

  • 關聯式資料庫需使用
  • 實作方式:用主鍵(Primary Key)、外鍵(Foreign Key)(指向另一個表的主鍵)連結
  • 種類
    • 一對一
    • 一對多
    • 多對多

一對多關聯

idtype
1pizza
2burger
3fries
idnameorder
1Jasonpizza
2Amberburger
3Heheburger
4Michaelfries

burger可以對應到很多個name要的 -> 一對多

Customers

Food

實作

class Food(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    type = db.Column(db.String(20), unique=True, nullable=False)
    ordered_by = db.relationship('Customers', backref='order', lazy="dynamic")

class Customers(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20), unique=True, nullable=False)
    food_id = db.Column(db.Integer, db.ForeignKey('food.id'), nullable=False)

建立與Customers的關係

backref:直接給Customers ''order''這個屬性

lazy="dynamic":print 出的會是一個list

Foreign Key

指向Food的id

上操作

with app.app_context():        
	food1 = Food(type="pizza")
	food2 = Food(type="burger")
	food3 = Food(type="fries")
	cus1 = Customers(name="Jason", order=food1)
	cus2 = Customers(name="Amber", order=food2)
	cus3 = Customers(name="Hehe", order=food2)
	cus4 = Customers(name="Michael", order=food3)
	db.session.add(food1)                        
	db.session.add(food2)
	db.session.add(food3)
	db.session.add(cus1) 
	db.session.add(cus2)
	db.session.add(cus3)
	db.session.add(cus4)
	db.session.commit()

上操作

with app.app_context():        
	food = Food.query.filter_by(type="burger").first()
	order_by = food.ordered_by.all() 
	for o in order_by:
		print(o.name)
with app.app_context():
	cus = Customers.query.filter_by(name="Hehe").first()
	print(cus.order.type)

正向查詢(從Food找ordered_by,看有哪些Customers)

反向查詢(從Customers.orders回去翻type,也就是food裡面的東東)

一對一關聯實作

你問我為什麼直接跳實作

因為只要在前面例子的db.relationship()裡面再加上uselist=False就可以了

多對多關聯

idtype
1pizza
2burger
3fries
idnameorder
1Jasonpizza, fries
2Amberburger
3Hehepizza, burger
4Michaelfries

Customers

Food

會需要透過「中繼表」來實作

?

實作

customer_foods = db.Table('customer_foods',
    db.Column('customer_id', db.Integer, db.ForeignKey('customers.id'), primary_key=True),
    db.Column('food_id', db.Integer, db.ForeignKey('food.id'), primary_key=True)
)

class Food(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    type = db.Column(db.String(20), unique=True, nullable=False)
    ordered_by = db.relationship('Customers', 
                                secondary=customer_foods, 
                                backref=db.backref('orders', lazy='dynamic'),
                                lazy='dynamic')

class Customers(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20), unique=True, nullable=False)

secondary指向customer_foods這個表

customer_foods:中繼表

移掉Foreign key,全都由中繼表處理

上操作

with app.app_context():
	pizza = Food(type="pizza")
	burger = Food(type="burger")
	fries = Food(type="fries")
	jason = Customers(name="Jason")
	amber = Customers(name="Amber")
	hehe = Customers(name="Hehe")
	michael = Customers(name="Michael")
	jason.orders.append(pizza)
	jason.orders.append(fries)
	amber.orders.append(burger)
	hehe.orders.append(pizza)
	hehe.orders.append(burger)
	michael.orders.append(fries)
	db.session.add_all([pizza, burger, fries])
	db.session.add_all([jason, amber, hehe, michael])
	db.session.commit()

上操作

with app.app_context():
	p = Food.query.filter_by(type="pizza").first()      
	for cus in p.ordered_by.all():     
	print(cus.name)
with app.app_context():
	cus = Customers.query.filter_by(name="Amber").first()      
	for p in cus.orders.all():     
	print(p.type)

正向查詢

反向查詢

Blueprints

前面的區域以後再來探索吧

好啦下周會講 講師在這邊道歉

Blueprints簡介

試想一下

我們前面實作會用的flask應用都只會有兩、三個route而已

但當今天應用規模變大變複雜 route變成一坨 你還會想把所有東西都放在app.py嗎

from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
	return "Hello World"
  
@app.route("/one")
def one():
   return "one"

@app.route("/two")
def two():
   return "two"

@app.route("/three")
def three():
   return "three"

#...

@app.route("/sixty_six")
def sixty_six():
   return "sixty six"

@app.route("/sixty_seven")
def sixty_seven():
   return "sixty seven"
  
if __name__ == "__main__":
	app.run(debug=True,port=8080)

app.py

Blueprints簡介

所以你打算把一些route分到別的檔案再import進來

from flask import Flask
import one

app = Flask(__name__)

@app.route("/")
def index():
	return "Hello World"
  
if __name__ == "__main__":
	app.run(debug=True,port=8080)
from app import app

@app.route("/one")
def one():
   return "one"

@app.route("/two")
def two():
   return "two"

@app.route("/three")
def three():
   return "three"

#...

@app.route("/sixty_six")
def sixty_six():
   return "sixty six"

@app.route("/sixty_seven")
def sixty_seven():
   return "sixty seven"

app.py

one.py

Blueprints簡介

然後他就死掉了

Blueprints簡介

實際上這東西叫circular import,是python直譯的機制間接造成的問題

from flask import Flask
import one
from app import app

兩個檔案會反覆橫跳

app.py

one.py

Blueprints簡介

有這些方法可以處理:

  • 用函式,可以把import語句包在裡面or以參數的方式傳入
  • 再創一個module放共同要用的東東
  • 整個module import進來,不要寫from ......

Blueprints簡介

講回來,官方推薦用blueprints解決專案規模越來越大的問題

Blueprints簡介

  • 是一種Flask推薦,用於構建、擴展應用的方法
  • 優點
    • 有利於大型專案和團隊合作
    • 有效的組織,增強代碼可讀性和維護性
    • 可以用這個改URL前綴,以防route之間衝突
    • 可共享靜態文件、模板等等
  • 缺點
    • 套用在應用上然後應用創建的時候,要改只能全都砍掉
from flask import Blueprint

一個簡單的實例

from flask import Blueprint
blueprint = Blueprint('one', __name__)

@blueprint.route('/one')
def index():
    return "one"
from flask import Flask
from one import blueprint

app = Flask(__name__)
app.register_blueprint(blueprint)

@app.route("/")
def index():
	return "Hello World"

if __name__ == "__main__":
	app.run(debug=True)

one.py

app.py

實體化

import進來

「註冊」:把blueprint裡的操作套用在app上

*這兩個步驟是必需的

一個簡單的實例

巢狀藍圖

可以在一個藍圖上註冊另一個藍圖

parent = Blueprint('parent', __name__, url_prefix='/parent')
child = Blueprint('child', __name__, url_prefix='/child')
parent.register_blueprint(child)
app.register_blueprint(parent)

child這個藍圖裡面如果有個route叫create

那URL會變成/parent/child/create

一些進階參數設定

url_prefix

blueprint會用url_prefix來實作

https://www.youtube.com/@changyi/shorts

常會看到這種url

from flask import Blueprint
blueprint = Blueprint('one', __name__, url_prefix='/numbers')

@blueprint.route('/one')
def index():
    return "one"

template_folder

  • 用blueprint切網站的方式有兩種:依功能/路徑分
  • 功能:根據不同功能用blueprint分,每個有自己的route、templates、static等等
  • 路徑:只切到route,剩下的都只各分在一個大資料夾

有分出專屬的templates

template_folder

blueprint會用template_folder來實作

找到他對應的template

結構

from flask import Blueprint, render_template
blueprint = Blueprint('one', __name__, url_prefix='/numbers',template_folder='templates')

@blueprint.route('/one')
def index():
    return render_template('one/index.html')

one/one.py

template_folder

從blueprint外面找他的模板的話,會在前面加(blueprint名字).

e.g. 要找one的index.html就會變成url_for(one.index)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <a href="{{ url_for('one.index') }}">點我</a>
</body>
</html>

static_folder

用法跟template_folder一模一樣,就不舉例了

admin = Blueprint('admin', __name__, static_folder='static')

長得一樣

其他套件介紹

Flask-WTF

  • 不是你想的那樣子
  • 幫你寫好的表單功能
  • 有input&資料驗證&防CSRF
pip install Flask-WTF
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length, Email

class LoginForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
    submit = SubmitField('Login')
from flask import Flask, render_template, redirect, url_for, flash
from forms import LoginForm

app = Flask(__name__)
app.config['SECRET_KEY'] = '6767'

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        flash(f'成功登入!Email: {form.email.data}')
        return redirect(url_for('login'))
        
    return render_template('login.html', form=form)

if __name__ == '__main__':
    app.run(debug=True)

上:app.py 下:forms.py

Flask-WTF

<!DOCTYPE html>
<html>
<head>
    <title>Login</title>
</head>
<body>
    <h2>登入</h2>
    
    <form method="POST" action="">
        {{ form.hidden_tag() }}
        
        <div>
            {{ form.email.label }}<br>
            {{ form.email(placeholder="example@mail.com") }}<br>
            {% for error in form.email.errors %}
                <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </div>
        <br>
        <div>
            {{ form.password.label }}<br>
            {{ form.password() }}<br>
            {% for error in form.password.errors %}
                <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </div>
        <br>
        <div>
            {{ form.submit() }}
        </div>
    </form>
</body>
</html>

login.html

Flask-login

  • 直接幫你管理session,不用自己寫
  • 主要
    • LoginManager:主要控制
    • UserMixin:創用戶的模型會需要繼承的
    • user_loader:告訴套件怎麼從資料庫查該用戶物件
  • 其他實用功能
    • login_user(user)、logout_user():登入登出
    • @login_required:裝飾器,登入才能看的頁面會加
    • current_user:代表當前登入用戶的變數

Flask-login

這個我不太熟,詳細的可以看官方文件

Flask-bcrypt

  • 密碼雜湊&驗證
pip install flask-bcrypt
from flask_bcrypt import Bcrypt
bcrypt = Bcrypt(app)

pw_hash = bcrypt.generate_password_hash('password').decode('utf-8')
is_valid = bcrypt.check_password_hash(pw_hash, 'password')

加密

驗證(True/False)

上完了 噎

Made with Slides.com