Python Web Flask 對JSON的存取 — 2從資料庫內容匯出(下)
上篇我們提到管理資料庫資料CRUD功能中的C與R,在這裡我們要繼續從U開始說明。並且討論如何增加搜尋、排序以及匯出JSON檔案的功能。我們就繼續開始。
編輯資料庫的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">×</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)。
在這樣的前提下會讓我不斷的思考,目前生活周遭是否有其他需要被改善的地方?這些問題是否可以透過撰寫程式碼來解決?或者是對於現有的系統,是否可以透過改寫或者是添加程式碼來讓效能更優化?
以這次的問題來說,我們透過改寫原本的程式碼,用更好更合適的方法來重構程式,來讓系統的功能變得更佳的實用。
只要心存這樣的想法,你就會發現生活中有很多地方可以啟發您產生新的構想。