在視圖函數中操作數據庫
在視圖函數中操作數據庫的方式和在python shell中的練習基本相同,只不過需要一些額外的工作。比如把查詢結果作爲參數傳入模板渲染出來,或是獲取表單的字段值作爲提交到數據庫的數據。接下來,我們將實現用來創建、編輯和刪除筆記並在主頁列出所有保存後筆記的程序。
create
爲了支持輸入筆記內容,需要先創建一個用於填寫筆記的表單,如下所示:
app.py:
# encoding=utf-8
import os
import click
from flask import Flask, flash, url_for, request, render_template
from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField
from wtforms.validators import DataRequired
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY', 'secret string')
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL",
'sqlite:///'+os.path.join(app.root_path, "data.db"))
db = SQLAlchemy(app)
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
def __repr__(self):
# %r是用repr()方法處理對象,返回類型本身,而不進行類型轉換
return "<Note %r>" % self.body
class NewNoteForm(FlaskForm):
body = TextAreaField('Body', validators=[DataRequired()])
submit = SubmitField('Save')
def initdb():
db.create_all()
@app.route('/new', methods=['POST','GET'])
def new_note():
form = NewNoteForm()
print("form: ", form)
print("form.validate_on_submit(): ", form.validate_on_submit())
if form.validate_on_submit():
print("pass")
try:
print(Note.query.all())
except:
print("initdb...")
initdb()
body = form.body.data
note = Note(body=body)
db.session.add(note)
db.session.commit()
flash('Your note is saved.')
return render_template('hi.html', form=form)
return render_template('new_note.html', form=form)
@app.route("/hi")
def hi():
return render_template("hi.html")
if __name__ == "__main__":
app.run(debug=True)
當form.validate_on_submit()發貨True時的處理代碼,當表單被提交且驗證通過時,我們獲取表單body字段的數據,然後創建新的Note實例,將表單中的body字段的值作爲body參數傳入,最後添加到數據庫會話中並提交會話。這個過程接收用戶通過表單提交的數據並保存到數據庫中,最後我們使用flash()函數發送提交消息並重定向到hi視圖。
new_note.html:
{
% extends "base.html" %}
{% from "macro.html" import form_field %}
{% block content %}
<h2>New Note</h2>
<form method="post">
{{ form.csrf_token }}
{{ form_field(form.body, rows=4, cols=50) }}
{{ form.submit }}
</form>
{% endblock %}
表單在new_note.html模板中渲染,這裏使用我們之前學的form_field渲染表單字段,傳入rows和cols參數來定製<textarea>輸入框的大小
macro.html:
{% macro form_field(field) %}
{{ field.label }}<br>
{{ field(**kwargs) }}<br>
{% if field.errors %}
{% for error in field.errors %}
<small class="error">{{ error }}</small>
{% endfor %}
{% endif %}
{% endmacro %}
hi.html:用來顯示主頁,目前它所有的作用就是渲染主頁對應的模板
{% extends "base.html" %}
{% block content %}
<title>Hello from Flask</title>
<ul>
<li><a href="{{ url_for('new_note') }}">new note</a></li>
</ul>
{% endblock %}
base.html:
<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
<meta charset="UTF-8">
<title>{% block title %}Template - HelloFlask{% endblock %}</title>
{% block styles %}
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{% endblock %}
{% endblock %}
</head>
<body>
<nav>
<ul><li><a href="{{ url_for('hi') }}">Home</a></li></ul>
</nav>
<main>
{% for message in get_flashed_messages() %}
<div class="alert">{{ message }}</div>
{% endfor %}
{% block content %}
{% endblock %}
</main>
<footer>
{% block footer %}
<small>©: 2019
<a href="https://blog.csdn.net/kongsuhongbaby?t=1",title="kongsh's blog">孔素紅的博客</a>/
<a href="https://github.com/kongsh8778/" title="contact me on Github">Github</a>/
<a href="http://helloflask.com", title="A helloflask project">Learning helloflask</a>
</small>
{% endblock %}
</footer>
{% block scripts %}
{% endblock %}
</body>
</html>
瀏覽器訪問:http://127.0.0.1:5000/hi
單擊 new note,輸入內容然後保存
又重新跳轉到hi視圖
創建數據後,通過命令行或客戶端查看note表是否有數據
read
上面的程序實現了添加筆記的功能,在創建比較頁面單擊保存後,程序會重定向到主頁,提示的消息告訴你剛剛提交的比較已經保存,這時無法看到創建後的筆記。爲了在主頁列出所有保存的筆記,需要修改hi視圖。
app.py:
@app.route("/hi")
def hi():
form = NewNoteForm
notes = Note.query.all()
return render_template("hi.html", notes=notes, form=form)
在新的index視圖中,我們使用Note.query.all()查詢所有的note記錄,然後把這個包含所有記錄的列表作爲notes變量傳入模板,接下來在模板中顯示。
hi.html:處理視圖函數傳進來的notes,notes|length是過濾器,相當於python的len(notes)
{% extends "base.html" %}
{% block content %}
<title>Hello from Flask</title>
<h1>Notebook</h1>
<a href="{{ url_for('new_note') }}">new note</a>
<h4>{{ notes|length }} notes:</h4>
{% for note in notes %}
<div class="note">
<p>{{ note.body }}</p>
</div>
{% endfor %}
{% endblock %}
在模板中,變量這個notes列表,調用Note對象的body屬性(note.body)獲取body字段的值。通過length過濾器獲取筆記的數量
瀏覽器訪問:http://127.0.0.1:5000/hi
update
更新一條筆記和創建一條新筆記的代碼基本相同,首先是定義編輯筆記的表單類:
class EditNoteForm(FlaskForm):
body = TextAreaField('Body', validators=[DataRequired()])
submit = SubmitField('Update')
這個類和創建筆記的類NewNoteForm的不同之處就是提交字段的標籤參數(作爲<input>的value屬性),因此這個表單的定義也可以通過繼承來簡化:
class EditNoteForm(NewNoteForm):
submit = SubmitField('Update')
app.py增加edit_note視圖:
@app.route("/edit/<int:note_id>", methods=['POST', 'GET'])
def edit_note(note_id):
form = EditNoteForm()
print("form.body:", form.body)
print("form.body.data:", form.body.data)
note = Note.query.get(note_id)
print("note.body:", note.body)
if form.validate_on_submit():
print("validated")
note.body = form.body.data
print("note.body is validate:%s ", note.body)
db.session.commit()
flash("your note is updated.")
return redirect(url_for("hi"))
# get請求的處理流程
form.body.data = note.body
return render_template('edit_note.html', form=form)
@app.route("/hi")
def hi():
form = NewNoteForm
notes = Note.query.all()
return render_template("hi.html", notes=notes, form=form, note_id=3)
這個視圖通過URL變量note_id獲取要被修改的筆記的主鍵值(id字段),然後就可以使用get()方法獲取對應的Note實例,當表單被提交併且通過驗證時,將表單中body字段的值賦值給note對象的body屬性,然後提交數據庫會話,這樣就完成了更新操作。然後flask一個提示消息並重定向到hi視圖。需要注意的是,在GET請求的執行過程中,添加了下面的代碼:
form.body.data = note.body
因爲要添加修改筆記內容的功能,那麼當打開修改某個筆記的頁面時,這個頁面的表單中必然要包含原有的內容。如手動創建HTML表單,那麼可以通過將note記錄傳入模板,然後手動爲對應字段中填入筆記的原有內容,如:
<textarea name="body">{{ note.body }}</textarea>
其它input元素則通過value屬性來設置輸入框中的值,如:
<input name="foo" type="text" value="{{ note.title }}">
使用input元素可以省略這些步驟,當我們在渲染表單字段時,如果表單字段的data屬性不爲空,WTForms會自動把data屬性的值添加到表單字段的value屬性中,作爲表單的值填充進去,不用手動爲value屬性賦值。因此,將存儲筆記原因內容的note.body屬性賦值給表單字段的data屬性即可在頁面上的表單中填入原有的內容。
edit_note.html:
{% extends "base.html" %}
{% from "macro.html" import form_field %}
{% block title %}Edit Note {% endblock %}
{% block content %}
<h2>Edit Note</h2>
<form method="post">
{{ form.csrf_token }}
{{ form_field(form.body, rows=5,cols=50) }}
{{ form.submit }}<br>
</form>
{% endblock %}
hi.html:在主頁筆記列表中的每個筆記內容下添加edit按鈕用來訪問編輯頁面
{% extends "base.html" %}
{% block content %}
<title>Hello from Flask</title>
<h1>Notebook</h1>
<a href="{{ url_for('new_note') }}">new note</a>
<h4>{{ notes|length }} notes:</h4>
{% for note in notes %}
<div class="note">
<p>{{ note.body }}</p>
<a class="btn" href="{{ url_for('edit_note',note_id=note_id) }}">Edit</a>
</div>
{% endfor %}
{% endblock %}
瀏覽器訪問:http://127.0.0.1:5000/hi
單擊edit
生成edit_note視圖的URL時,我們傳入當前note對象的id(note.id)作爲URL變量note_id的值
delete
在程序中刪除的實現也比較簡單,不過有一個誤區,通常的考慮是在筆記的內容下添加一個刪除鏈接:
<a href=”{{ url_for(‘delete_note’, note_id=note.id) }}”>Delete</a>
這個鏈接指向用來刪除比較的delete_note視圖:
@app.route("/delete/<int:note_id>")
def delete_note(note_id):
note = Note.query.get(note_id)
db.session.delete(note)
db.session.commit()
flash("your note is deleted.")
return redirect(url_for("hi"))
雖然這樣看起來很合理,但是這種方式會是程序處於CSRF攻擊的風險之中。之前提過,防範CSRF攻擊的基本原則就是正確的視野GET和POST方法,像刪除這類修改數據的操作絕對不能通過GET請求來實現,正確的做法是爲刪除操作創建一個表單,如下:
class DeleteNoteForm(FlaskForm):
submit = SubmitField('Delete')
這個表單類只有一個提交字段,因爲我們只需要在頁面上顯示一個刪除按鈕來提交表單。刪除表單的提交請求由delete_note視圖處理
@app.route("/delete/<int:note_id>", methods=["POST"])
def delete_note(note_id):
form = DeleteNoteForm()
if form.validate_on_submit():
# 獲取對應的記錄
note = Note.query.get(note_id)
# 刪除記錄
db.session.delete(note)
# 提交到會話
db.session.commit()
flash("your note is deleted.")
else:
abort(400)
return redirect(url_for("hi"))
在delete_note視圖的app.route()中,methods列表僅填入了POST,這樣就確保該視圖僅監聽POST請求。和編輯筆記的視圖類似,這個視圖接收note_id作爲參數。如果提交表單且驗證通過(唯一需要被驗證的是CSRF令牌),就用get()方法查詢對應的記錄,然後調用db.session.delete()方法刪除並提交數據庫會話。如果驗證出錯則使用abort()函數返回400錯誤響應碼。
因爲刪除按鈕要在主頁的筆記內容下添加,我們需要在hi視圖中實例化DeleteNoteForm類,然後傳入模板,在hi.html中渲染這個表單:
hi.html:
{% extends "base.html" %}
{% block content %}
<title>Hello from Flask</title>
<h1>Notebook</h1>
<a href="{{ url_for('new_note') }}">new note</a>
<h4>{{ notes|length }} notes:</h4>
{% for note in notes %}
<div class="note">
<p>{{ note.body }}</p>
<a class="btn" href="{{ url_for('edit_note',note_id=note_id) }}">Edit</a>
<form method="post" action="{{ url_for('delete_note', note_id=note_id) }}">
{{ form_delete.csrf_token }}
{{ form_delete.submit(class="btn") }}
</form>
</div>
{% endfor %}
我們將表單的action屬性設置我刪除當前筆記的URL。構建URL時,URL變量note_id的值通過note.id屬性獲取,當單擊提交按鈕時,會將請求發送到action屬性的URL,添加刪除表單的主要目的是防止CSRF攻擊,所以不要忘記渲染CSRF令牌字段form.csrf_token。
修改hi視圖,傳入刪除表單類,因爲hi模板中需要用的表單是刪除的表單:
@app.route("/hi")
def hi():
# form = NewNoteForm
form_delete = DeleteNoteForm
notes = Note.query.all()
return render_template("hi.html", notes=notes, form_delete=form_delete, note_id=3)
在HTML中,<a>標籤會顯示爲鏈接,而提交按鈕會顯示爲按鈕,爲了讓編輯和刪除筆記的按鈕顯示相同的樣式,我們爲這2個元素使用了同一個CSS類“.btn”
static/style.css:
body {
margin: auto;
width: 1100px;
}
nav ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: peru;
}
nav li {
float: left;
}
nav li a {
display: block;
color: white;
text-align: center;
padding: 14px 20px;
text-decoration: none;
}
nav li a:hover {
background-color: #111;
}
main {
padding: 10px 20px;
}
footer {
font-size: 13px;
color: #888;
border-top: 1px solid #eee;
margin-top: 25px;
text-align: center;
padding: 10px;
}
.alert {
position: relative;
padding: 0.75rem 1.25rem;
margin-bottom: 1rem;
border: 1px solid #b8daff;
border-radius: 0.25rem;
color: #004085;
background-color: #cce5ff;
}
.note p{
padding:10px;
border-left:solid 2px #bbb;
}
.note form{
display:inline;
}
.btn{
font-family:Arial;
font-size:14px;
padding:5px 10px;
text-decoration:none;
border:none;
background-color:white;
color:black;
border:2px solid #555555;
}
.btn:hover{
text-decoration:none;
background-color:black;
color:white;
border:2px solid black;
}
瀏覽器訪問:http://127.0.0.1:5000/hi
單擊delete後
完整的app.py:
# encoding=utf-8
import os
import click
from flask import Flask, flash, url_for, request, render_template, redirect, abort
from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField
from wtforms.validators import DataRequired
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY', 'secret string')
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL",
'sqlite:///'+os.path.join(app.root_path, "data.db"))
db = SQLAlchemy(app)
class Note(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
def __repr__(self):
# %r是用repr()方法處理對象,返回類型本身,而不進行類型轉換
return "<Note %r>" % self.body
class NewNoteForm(FlaskForm):
body = TextAreaField('Body', validators=[DataRequired()])
submit = SubmitField('Save')
class EditNoteForm(FlaskForm):
body = TextAreaField('Body', validators=[DataRequired()])
submit = SubmitField('Update')
class DeleteNoteForm(FlaskForm):
submit = SubmitField('Delete')
# class EditNoteForm(NewNoteForm):
# submit = SubmitField('Update')
def initdb():
db.create_all()
@app.route('/new', methods=['POST','GET'])
def new_note():
form = NewNoteForm()
print("form: ", form)
print("form.validate_on_submit(): ", form.validate_on_submit())
if form.validate_on_submit():
print("pass")
try:
print(Note.query.all())
except:
print("initdb...")
initdb()
body = form.body.data
note = Note(body=body)
db.session.add(note)
db.session.commit()
flash('Your note is saved.')
return render_template('hi.html', form=form)
return render_template('new_note.html', form=form)
@app.route("/edit/<int:note_id>", methods=['POST', 'GET'])
def edit_note(note_id):
form = EditNoteForm()
print("form.body:", form.body)
print("form.body.data:", form.body.data)
note = Note.query.get(note_id)
print("note.body:", note.body)
if form.validate_on_submit():
print("validated")
note.body = form.body.data
print("note.body is validate:%s ", note.body)
db.session.commit()
flash("your note is updated.")
return redirect(url_for("hi"))
# get請求的處理流程
form.body.data = note.body
return render_template('edit_note.html', form=form)
@app.route("/delete/<int:note_id>", methods=["POST"])
def delete_note(note_id):
form = DeleteNoteForm()
if form.validate_on_submit():
# 獲取對應的記錄
note = Note.query.get(note_id)
# 刪除記錄
db.session.delete(note)
# 提交到會話
db.session.commit()
flash("your note is deleted.")
else:
abort(400)
return redirect(url_for("hi"))
@app.route("/hi")
def hi():
db.create_all()
form = NewNoteForm
form_delete = DeleteNoteForm()
notes = Note.query.all()
return render_template("hi.html", notes=notes, form=form, form_delete=form_delete, note_id=4)
if __name__ == "__main__":
app.run(debug=True)