flask(十五):數據庫(三)

在視圖函數中操作數據庫

在視圖函數中操作數據庫的方式和在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)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章