Python Web Flask 對JSON的存取 — 2從資料庫內容匯出(下)

Sean Yeh
Python Everywhere -from Beginner to Advanced
22 min readMar 10, 2021
Manila, Philippines, photo by Sean Yeh

編輯資料庫的Update View

有了按鈕,我們就可以來編寫對應的View。眼尖的人從home.html樣板上面的粗體字,就可以從編輯連結使用的url發現我們接下來要說明的update view的名稱。

<a class="btn btn-outline-success btn-sm" href="{{url_for('update_items_db',id=config.id)}}">編輯</a>

是的,就是update_items_db,路由也是一樣。與新增(create view)不同的地方是,這個路由多了<int:id>,且update_items_db也需要輸入id參數。

update_items_db的基本邏輯是:先依照輸入的id參數找尋資料庫中符合條件的紀錄,然後顯示出表單(UpdateItemForm)供使用者修改欄位的資料。當使用者進入這個表單的第一時間,尚未修改任何值(request為GET的狀態),原先在資料庫中儲存的值會被顯示在修改表單的欄位中。

form.item.data = config_item.item

一旦使用者修改資料完畢並按下確認按鈕之後(request為POST的狀態),資料庫中的item欄位值,就讓透過表單新輸入的值來取代。

config_item.item = form.item.data

資料儲存完畢後,引導(redirect)使用者回到首頁,也就是資料的列表,這時候首頁上會顯示出修改後的資料狀態。完整的update_items_db如下:

@app.route('/update_items_db/<int:id>',methods=['GET','POST'])def update_items_db(id):
"""修改 DB"""
config_item = Configuration.query.get_or_404(id)
form = UpdateItemForm()
if form.validate_on_submit():
config_item.item = form.item.data
db.session.commit()
flash(f'資料 #{config_item.id} 已被更新成 {config_item.item}')
return redirect(url_for('home'))
elif request.method =='GET':
form.item.data = config_item.item
return render_template('add_items_form.html',form=form)

至於我們為什麼會知道id為何?因為當我們按下編輯按鈕時,href順道將config.id帶過去update_items_db。

href="{{url_for('update_items_db',id=config.id)}}"

透過這個方式,我們才可以特定出要修改的是哪一筆資料。

頁面完成後,顯示如下的畫面:

刪除資料庫的Delete View

刪除也需要id來特定出我們想要刪除的是哪一筆資料。一旦確定id之後,就可以找到資料庫中對應id 的那筆資料(query.get_or_404(id)),然後就可以透過資料庫的刪除語法(db.session.delete)來刪除紀錄。

因此,我們設計了一個delete view,名稱為delete_items_db,路由的名稱也一樣。接著透過query.get_or_404的方式找到資料庫中的那筆紀錄,並指定給item變數。找到之後,就刪除該筆紀錄( db.session.delete(item) ),db.session.commit(),然後將畫面轉向首頁(home)結束整個流程。以下就是這個流程需要用到的view程式碼。

@app.route('/delete_items_db/<int:id>',methods=['POST'])def delete_items_db(id):    """刪除 DB"""
item = Configuration.query.get_or_404(id)
db.session.delete(item)
db.session.commit()
flash(f'資料 #{item.id} -{item.item}已刪除!!!!')
return redirect(url_for('home'))

刪除資料的Modal

完成刪除資料的delete view後,我們必須回到版型的部分。還記得之前我們在首頁的home.html中設計了一個按鈕(button)。當這個按鈕點下去的時候,將會顯示一個彈跳視窗(popup menu),在此我們使用Bootstrap的Modal components在這裡我們使用Bootstrap4.6版的元件)。透過這個元件,可以讓您輕鬆的完成一個還算美麗的彈跳視窗。

<button type="button" class="btn btn-outline-danger btn-sm"data-toggle="modal" data-target="#del_model-{{config.id}}">刪除</button>

結果預計會在瀏覽器中出現下面的一個畫面。

好的,我們從最原始的開始,還記得我們使用的一組如下面程式碼的for回圈把資料庫中的資料顯示出來:

<ul class="list-group list-group-flush">    {% for config in config_items %}    <li class="list-group-item">#{{config.id}}-{{config.item}}</li>    {% endfor %}</ul>

接著,我們把兩個按鈕放在迴圈中。換句話說,就是在for迴圈中的<li> HTML標籤裡面,放入兩個按鈕。分別是「編輯」與「刪除」。其中「編輯」的部分很單純,只需要一個<a>連結標籤就可以了,而「刪除」的部分,我們要使用彈跳視窗來顯示警告,並且執行刪除行為。

是以,在<li> HTML標籤裡面我們還需要再加上彈跳視窗的語法進去。也就是上一篇我們用 delete box 註解表示的地方。

<!-- delete box --><div class="modal" tabindex="-1" id="del_model-{{config.id}}">   ...省略...</div><!--// delete box -->

我們可以從Bootstrap網站拷貝程式碼貼進這個地方,並且依照我們的需要修改一下這程式碼。

<!-- delete box --><div class="modal" tabindex="-1" id="del_model-{{config.id}}">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">刪除設定?</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>刪除設定,您確定要刪除這筆紀錄?</p>
<p> {{config.id}} - {{config.item}} </p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<form action="{{url_for('delete_items_db',id=config.id)}}" method="post">
<input class="btn btn-danger" type="submit" value="確定刪除">
</form>
</div>
</div>
</div>
</div>
<!--// delete box -->

可以注意的地方是,我們在這個彈跳視窗中,放入將被刪除資料的訊息,作為刪除前的提醒。

{{config.id}} - {{config.item}}

並且,我們把確定刪除的按鈕做成一個form來進行刪除工作。form的action為delete_items_db並且帶id參數過去。

<form action="{{url_for('delete_items_db',id=config.id)}}" method="post">    <input class="btn btn-danger" type="submit" value="確定刪除"></form>

在使用的時候就會呈現下面的結果。

搜尋功能

一般稍微有規模的網站,都會設置一個搜尋的功能,提供進入網站的朋友,方便的找到想要的東西。雖然我們這個網站不算龐大,但是我們需要一個可以快速查找資料,以確定是否輸入有所遺漏或誤植。因此,我們也要來製作一個搜尋功能。

HTML部分

跟彈跳視窗一樣我們從Bootstrap網站中找到如下畫面一般適合的模板套用。在網站的右上方的搜尋框框,旁邊並且有個按鈕,用來觸發搜尋行為。

這個部分就放在我們的base.html樣板中。我們還需要在這個HTML樣板中進行微調。

<form class="form-inline mt-2 mt-md-0" method="GET"><!-- 表單內容 -->
<input class="form-control mr-sm-2" type="text" placeholder="Search" name="q" aria-label="搜尋" value="">
<button class="btn btn-outline-secondary my-2 my-sm-0" type="submit">搜尋</button></form>

我們在input的地方加上一個「q」的name屬性,讓使用者輸入字串進行搜尋時,程式可以抓到相關字串。

調整home View

在目前的home view上面(如下程式碼),我們需要進行一些調整,讓搜尋的結果可以呈現在這個地方。

@app.route('/')
def home():
"""首頁,列表"""
item_order = Configuration.id.desc()
config_items = Configuration.query.filter(item_order)
return render_template('home.html',config_items=config_items)

我們希望使用者初次造訪網站瀏覽時看到的是全部的項目,一旦使用者操作頁面左上方的搜尋功能時,在同樣的列表處,改為顯示搜尋的結果。

基於這個理由,首先我們要取得HTML頁面上使用者輸入的字串:

q = request.args.get('q')

可以使用request.args.get的方式來取得以「q」為名的input中所取得的字串。

取得字串後,我們使用if條件判斷來分開兩個不同的情境:

  • 如果取得「q」的字串,就利用這個字串到Configuration資料表的item欄位中找尋有此字串的紀錄。
  • 如果沒有取得「q」的字串,就維持原樣。把所有的資料紀錄以item_orders的順序展現出來。(在這裡我們設定為逆向排列,也就是資料越新的,越排在前面)

我們可以把上面的邏輯寫成下面的程式碼,其中粗體字的部分就是這次新增、修改的部分:

@app.route('/')def home():
"""首頁,列表"""
# get string from form as q
q = request.args.get('q')
item_orders = Configuration.id.desc() # if q existed
if q:
# filter by q
config_items = Configuration.query.filter(Configuration.item.contains(q))
else:
config_items = Configuration.query.order_by(item_orders)
return render_template('home.html',config_items=config_items)

排序功能

完成搜尋之後,我們可以再次優化一下這個頁面,讓這個頁面可以提供排序的功能。

問題思考

前面我們曾經稍稍提到列表維持原樣就是把所有的資料紀錄以item_orders的順序展現出來。

透過程式碼我們可以觀察發現,item_orders的順序是依照Configuration資料表中的id欄位為逆向排序(desc):

item_orders = Configuration.id.desc()

如果我們希望依照Configuration資料表中的id欄位為順向排序的話,只要將desc改成asc就可以實現。

item_orders = Configuration.item.asc()

那麼,我們可以會進一步思考,使否可以讓使用者透過網頁介面來控制這個值?

form.py

首先,我們需要一個表單,表單裡面有下拉選單可以調整控制排序的順序。因此,我們先從forms.py開始,建立一個名稱為OrderingForm的表單。在這裡我們使用的是下拉選單(SelectField),因此可以輸入各種的選項(choices),例如:1為正向’A-Z (ASC)、2為反向Z-A (DESC)。

class OrderingForm(FlaskForm):    order_type = SelectField(u'選擇資料排序方式',    validators=[DataRequired('選擇資料排序方式')],    choices=[(1,'A-Z (ASC)'),(2,'Z-A (DESC)')],    coerce=int)    submit = SubmitField('開始排序')

修改home view

在這裡我們需要先匯入OrderingForm,才可以讓home View與forms.py裡面的OrderingForm產生關聯性。

from configmaker.forms import AddItemForm,UpdateItemForm,OrderingForm

再調整home view,把form加入這個view裡面:

@app.route('/')def home():
"""首頁,列表"""
form = OrderingForm()
# get string from form as q
q = request.args.get('q')
item_orders = Configuration.id.desc()
# if q existed
if q:
# filter by q
config_items = Configuration.query.filter(Configuration.item.contains(q))
else:
config_items = Configuration.query.order_by(item_orders)
return render_template('home.html',config_items=config_items, form=form)

上面程式碼的粗體字部分就是我們加入form的地方。

HTML頁面

最後要調整HTML頁面。我們要在home.html樣板中,加上一個<form>標籤。讓使用者按下按鈕之後,就會啟動order_home的表單函式(action=”{{url_for(‘order_home’)}}”)。下面的HTML語法,由於使用了Bootstrap框架,看起來比較複雜,在最精簡的狀況下只需要粗體字的部分就可以完成這個功能。

<!-- 排序選單 -->
<div class="col-md-5">
<form action="{{url_for('order_home')}}" method="POST">
{{form.hidden_tag()}}
<div class="form-row mt-3 mb-3 mt-md-0">
<div class="col-8">
{{form.order_type.label(class='sr-only')} {{form.order_type(class='custom-select')}}
</div>
<div class="col">
{{form.submit(class='btn btn-primary btn-block')}}
</div>
</div>
</form>
</div>
<!-- //排序選單 -->

完成後,將顯示如下畫面:

右邊出現了下拉選單:

匯出JSON檔案

如下圖:

我們希望使用者按下「產生JSON」按鈕,就會自動產生一個以畫面中顯示項目為內容的JSON檔案(格式如下),並且存在我們指定的資料夾裡面,JSON檔案的檔名也是我們固定的。

["a this item is important","b the second item is also important","c class is beautiful"]

做法上,我們可以把先前設計的「匯出JSON檔案」的功能微調之後,加入這個網站裡面。

還記得在上篇中我們已經製作了一個可以將資料寫入JSON檔案的函式。該函式的名稱為write_to_json,以及函式內部呼叫的子函式_make_str_for_dump與_dump_json。其原始碼如下:

write_to_json函式

以下是原來的write_to_json函式原始碼:

def write_to_json(datas):
#檢查資料夾是否存在?
des_folder=os.path.join(basedir,'config') if not os.path.exists(des_folder):
os.makedirs(des_folder)
print(f"建立資料夾{des_folder}")
config_filename = 'config_data.json' config_file_url = os.path.join(basedir,'config',config_filename) with open(config_file_url, 'w') as config_file:
data_set = _make_str_for_dump(datas)
j_data =_dump_json(data_set)
config_file.write(j_data)

_make_str_for_dump函式

以下是原來的_make_str_for_dump函式原始碼:

def _make_str_for_dump(strings):
str_for_dump = []
for str in strings:
str_for_dump.append(str)
return str_for_dump

_dump_json函式

以下是原來的_dump_json函式原始碼:

def _dump_json(dic_data):
return json.dumps(dic_data, indent=4)

在此我們要利用這些函式,在目前的網站中加入「產生JSON」的功能。

db_to_json view

為了實現這個目的,我們要建立一個db_to_json view來處理相關問題。這個view會搜集目前資料庫中所有item欄位中的值,把它們集合起來,透過for迴圈逐一放入data中。搜集完畢後,再利用我們先前寫好的write_to_json函式,把data轉換為JSON並且存入filename中。為了讓JSON檔名可以依照不同專案的需要調整,我們在這裡修改了write_to_json函式,讓原本只有一個參數(datas)的函式,加上另外一個filename的參數。write_to_json函式的程式碼改成下面狀態:

def write_to_json(datas, config_filename):
"""存成JSON檔案"""
des_folder=os.path.join(basedir,'config')
if not os.path.exists(des_folder):
os.makedirs(des_folder)
print(f"建立資料夾{des_folder}")
#config_filename = 'config_data2.json'
config_file_url = os.path.join(basedir,'config',config_filename)
with open(config_file_url, 'w') as config_file:
data_set = _make_str_for_dump(datas)
j_data =_dump_json(data_set)
config_file.write(j_data)

我們把原本函式裡面的config_filename = ‘config_data2.json’刪除,改由透過外來的參數來決定config_filename的值。

再回到db_to_json程式碼,我們加入了data串列(data =[])來搜集資料庫的item值,並且把匯出的JSON檔名定義在這個函式裡面(filename= ‘ga_media_lists.json’)。接著把資料庫中所有的內容找出來(Configuration.query.all())存在config_items變數中。

再透過for迴圈存到data裡面。

for item in config_items:
data.append(str(item))

然後把data寫入JSON中。(write_to_json(data, filename)

@app.route('/db_to_json',methods=['GET','POST'])def db_to_json():
"""output to json取得JSON檔案"""
data =[]
filename='ga_media_lists.json'
config_items = Configuration.query.all()
for item in config_items:
data.append(str(item))
write_to_json(data,filename)
flash(f'已經產出JSON檔案{filename},請到config檔案夾取用')
return redirect(url_for('home'))

上面是完整的db_to_json函式。在這裡我們加上了flash提示,如下面的畫面所顯示,只要使用者按下按鈕產生JSON檔案後,就會在頁面上顯示提示訊息『已經產出JSON檔案【檔名】,請到config檔案夾取用』。

提示訊息

網站啟動後,就可以實際的測試看看每項功能是否都可以運作。

結語

學習寫程式對我個人來說,是希望透過程式撰寫來解決日常生活中碰到的實際問題,讓我們的生活可以更佳舒適( to Make Your Life Easier)。

在這樣的前提下會讓我不斷的思考,目前生活周遭是否有其他需要被改善的地方?這些問題是否可以透過撰寫程式碼來解決?或者是對於現有的系統,是否可以透過改寫或者是添加程式碼來讓效能更優化?

以這次的問題來說,我們透過改寫原本的程式碼,用更好更合適的方法來重構程式,來讓系統的功能變得更佳的實用。

只要心存這樣的想法,你就會發現生活中有很多地方可以啟發您產生新的構想。

--

--

Sean Yeh
Python Everywhere -from Beginner to Advanced

# Taipei, Internet Digital Advertising,透過寫作讓我們回想過去、理解現在並思考未來。並樂於分享,這才是最大贏家。