flask 在視圖函數裏操作數據庫
在視圖函數裏操作數據庫
在視圖函數裏操作數據的方式和在python shell中的聯系基本相同,只不過需要一些額外的工作。比如把查詢結果作為參數 傳入模板渲染出來,或是獲取表單的字段值作為提交到數據庫的數據。接下來,我們將把前面學習的所有數據庫操作知識運用到一個簡單的筆記程序中。這個程序可以用來創建、編輯和刪除筆記,並在主頁列出所有保存後的筆記。
1、Create
為了支持輸入筆記內容,需要先創建一個用於填寫筆記的表單,如下所示:
from flask_wtf import FlashForm from wtforms import TextAreaField, SubmitFieldfrom wtforms.validators import DataRequired class NewNoteForm(FlaskForm): body = TextAreaField(‘Body‘, validators=[DataRequired()]) submit = SubmitField(‘Save‘)
然後創建了一個new_note視圖,負責渲染創建筆記的模板,並處理表單的提交,如下所示:
@app.route(‘/new‘, methods = [‘GET‘, ‘POST‘]) def new_note(): form= NewNoteForm() if form.validate_on_submit(): body = form.body.data note = Note(body = body) db.session.add(note) db.session.commit() flash(‘Your note is saved.‘) return redirect(url_for(‘index‘)) return render_template(‘new_note.html‘, form = form)
先來看下form.validate_on_submit()返回True時的處理代碼,當表單被提交並且通過驗證時,我們獲取表單body字段的數據,然後創建新的Note實例,將表單中body字段的值作為body參數傳入,最後添加到數據庫會話中並提交會話。這個過程接收用戶通過表單提交的數據並保存到數據庫中,最後我們使用flash()函數發送提交消息並重定向到index視圖。
表單在new_note.html模板中渲染,這裏使用我們之前學的form_field渲染表單字段,傳入rows和cols參數來定制<textarea>輸入框的大小:
new_note.html:
% block content %} <h2>New Note</h2> <form method="post"> {{ form.csrf_token }} {{ form_field(form.body, rows=5, cols=50) }} {{ form.submit }} </form> {% endblock %}
index視圖用來顯示主頁,目前它的所有作用就是渲染主頁對應的模板:
@app.route(‘/index‘) def index(): return render_template(‘index.html‘)
index.html:
{% extends ‘base.html‘ %} {% block content %} <h1>Notebook</h1>> <a href="{{ url_for(‘new_note‘) }}">New Note</a> {% endblock %}
添加macros.html:
{% macro form_field(field) %} {{ field.label }}<br> {{ field(**kwargs) }}<br> {% if field.erros %} {% for error in field.errors %} <small class="error">{{ error }}</small><br> {% endfor %} {% endif %} {% endmacro %}
添加base.html:
<!DOCTYPE html> <html lang="en"> <head> {% block head %} {% block metas %} <meta charset="utf-8"> {% endblock metas %} <title>{% block title %} Database - HelloFlask {% endblock title %}</title> <link rel="stylesheet" type="text/css" href="{{ url_for(‘static‘, filename=‘favicon.ico‘) }}"> {% block styles %} <link rel="stylesheet" type="text/css" href="{{ url_for(‘static‘, filename=‘style.css‘) }}"> {% endblock styles %} {% endblock head %} </head> <body> <nav> {% block nav %} <ul> <li><a href="{{ url_for(‘index‘) }}">Home</a></li> </ul> {% endblock %} </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://www.cnblogs.com/xiaxiaoxu/" title="xiaxiaoxu‘s blog">夏曉旭的博客</a> / <a href="https://github.com/xiaxiaoxu/hybridDrivenTestFramework" title="Contact me on GitHub">GitHub</a> / <a href="http://helloflask.com" title="A HelloFlask project">Learning from GreyLi‘s HelloFlask</a> </small> {% endblock %} </footer> {% block scripts %}{% endblock %} </body> </html>
app.py裏面引入redirect,url_for,flash等模塊
#encoding=utf-8 from flask import Flask, render_template, flash, url_for, redirect from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) db = SQLAlchemy(app) import os app.secret_key = os.getenv(‘SECRET_KEY‘,‘secret string‘) import os app.config[‘SQLALCHEMY_DATABASE_URI‘] = os.getenv(‘DATABASE_URL‘, ‘sqlite:///‘ + os.path.join(app.root_path, ‘data.db‘)) app.config[‘SQLALCHEMY_TRACK_MODIFICATIONS‘] = False class Note(db.Model): id = db.Column(db.Integer, primary_key=True) db.Column() body = db.Column(db.Text) def __repr__(self): # %r是用repr()方法處理對象,返回類型本身,而不進行類型轉化 return ‘<Note %r>‘ % self.body import click @app.cli.command() def initdb(): db.create_all() click.echo(‘Initialized database.‘) from flask_wtf import FlaskForm from wtforms import TextAreaField, SubmitField from wtforms.validators import DataRequired class NewNoteForm(FlaskForm): body = TextAreaField(‘Body‘, validators=[DataRequired()]) submit = SubmitField(‘Save‘) @app.route(‘/new‘, methods = [‘GET‘, ‘POST‘]) def new_note(): form = NewNoteForm() print "form.validate_on_submit():",form print "form.validate_on_submit():",form.validate_on_submit() if form.validate_on_submit(): print "pass" body = form.body.data note = Note(body = body) db.session.add(note) db.session.commit() flash(‘Your note is saved.‘) return redirect(url_for(‘index‘)) return render_template(‘new_note.html‘, form = form) @app.route(‘/index‘) def index(): return render_template(‘index.html‘) if __name__ == ‘__main__‘: print app.config app.run(debug = True)
訪問127.0.0.1:5000/index:點擊create note,輸入內容,提交,頁面提示消息
2、Read
上面為程序實現了添加筆記的功能,在創建筆記頁面單擊保存後,程序會重定向到主頁,提示的消息告訴你剛剛提交的筆記已經保存了,這時無法看到創建後的筆記。為了在主頁列出所有保存的筆記,需要修改index視圖,
app.py: 在屬兔函數中查詢數據庫記錄並傳入模板
@app.route(‘/index‘) def index(): form = NewNoteForm notes = Note.query.all() return render_template(‘index.html‘, notes=notes, form=form)
在新的index視圖中,我們使用Note.query.all()查詢所有note記錄,然後把這個包含所有記錄的列表作為notes變量傳入模板,接下來在模板中顯示。
修改index.html: 處理視圖函數中傳進來的notes,notes|length是過濾器相當於python的len(notes),取notes的長度
{% extends ‘base.html‘ %} {% block content %} <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過濾器獲取筆記的數量。
渲染後的index頁面:
3、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=[‘GET‘, ‘POST‘]) def edit_note(note_id): form = EditNoteForm() note = Note.query.get(note_id) if form.validate_on_submit(): note.body = form.body.data db.session.commit() flash(‘Your note is updated.‘) return redirect(url_for(‘index‘)) form.body.data = note.body return render_template(‘edit_note.html‘, form = form)
這個視圖通過URL變量note_id獲取要被修改的筆記的主鍵值(id字段),然後我們就可以使用get()方法獲取對應的Note實例,當表單被提交且通過驗證時,將表單中body字段的值賦值給note對象的body屬性,然後提交數據庫會話,這樣就完成了更新操作,然後flash一個提示消息並重定向到index視圖。
需要註意的是,在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‘ %} {% 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 %}
最後再主頁筆記列表中的每個筆記內容下邊添加一個編輯按鈕,用來訪問編輯頁面
index.html:
{% extends ‘base.html‘ %} {% block content %} <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 %}
訪問:127.0.0.1:5000/index
點擊edit
生成edit_note視圖的URL時,我們傳入當前note對象的id(note.id)作為URL變量note_id的值。
4、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(‘index‘))
雖然這看起來很合理,但這種方式會使程序處於CSRF攻擊的風險之中。之前學過,防範CSRF攻擊的基本原則就是正確的使用GET和POST方法。像刪除這類修改數據的操作絕對不能通過GET請求來實現,正確的做法是為刪除操作創建一個表單,繼承自NewNoteForm(重寫submit字段),如下所示:
class DeleteNoteForm(FlaskForm): submit = SubmitField(‘Delete‘)
這個表單類只有一個提交字段,因為我們只需要在頁面上顯示一個刪除按鈕來提交表單。刪除表單的提交請求由delete_note視圖處理
@app.route(‘/delete/<int:note_id>‘, methods=[‘POST‘]) def delete_note(note_id): form = DeleteForm() 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(‘index‘))
在delete_note視圖的app.route()中,methods列表僅填入了POST,這會確保該視圖僅監聽POST請求。
和編輯筆記的視圖類似,這個視圖接收note_id(主鍵值)作為參數。如果提交表單且驗證通過(唯一需要被驗證的是CSRF令牌),就是用get()方法查詢對應的記錄,然後調用db.session.delete()方法刪除並提交數據庫會話。如果驗證出錯則使用abort()函數返回400錯誤響應。
因為刪除按鈕要在主頁的筆記內容下添加,我們需要在index視圖中實例化DeleteNoteForm類,然後傳入模板。在index.html模板中,渲染這個表單:
index.html:
{% extends ‘base.html‘ %} {% block content %} <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 %} {% endblock %}
我們將表單的action屬性設置為刪除當前筆記的URL。構建URL時,URL變量note_id的值通過note.id屬性獲取,當單機提交按鈕時,會將請求發送到action屬性中的URL。添加刪除表單的主要目的就是防止CSRF攻擊,所以不要忘記渲染CSRF令牌字段form.csrf_token。
修改index視圖,傳入刪除表單類,因為index模板中需要用的表單是刪除的表單:
@app.route(‘/index‘) def index(): #form = NewNoteForm() form_delete = DeleteNoteForm() notes = Note.query.all() return render_template(‘index.html‘, notes=notes, form_delete = form_delete)
在HTML中,<a>標簽會顯示為鏈接,而提交按鈕會顯示為按鈕,為了讓編輯和刪除筆記的按鈕顯示相同的樣式,我們為這兩個元素使用了同一個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; }
作為替代,也可以考慮用JavaScript創建監聽函數,當刪除按鈕按下時,提交對應的隱藏表單。
訪問127.0.0.1:5000/index
flask 在視圖函數裏操作數據庫