首先是去 model.py 修改之前做登入測試時就建立的 Members 資料模型, 添加了 email (預備發 email 用), 與 mobile (預備發簡訊用) 兩個欄位如下 :
class Members(db.Model):
account=db.StringProperty(required=True)
password=db.StringProperty(required=True)
theme=db.StringProperty(required=True,default="default")
is_admin=db.BooleanProperty(required=True,default=False)
email=db.EmailProperty(required=False)
mobile=db.StringProperty(required=False)
member=Members(key_name="admin",account="admin",password="admin",
theme="default",is_admin=True,email="foo@bar.com",mobile="0933")
member.put()
member=Members(key_name="guest",account="guest",password="guest",
theme="black",is_admin=False,email="foo@bar.com",mobile="0932")
member.put()
其次新增一個 Systabs 資料模型的實體, 以便增加一個 "使用者" 頁籤, 超連結指向 /list_members 這個路徑 :
systab=Systabs(key_name="list_members",tab_name="list_members",
tab_label=u"使用者",tab_link="/list_members",tab_order=3,tab_admin=True)
systab.put()
這樣就搞定資料儲存了, 接下來去 main.py 新增 /list_members 路徑的處理類別, 主要是用來顯示目前的使用者列表 :
class list_members(webapp2.RequestHandler):
def get(self):
#query Themes from datastore
themes=m.Themes.all()
info={}
theme_list=[]
for t in themes:
theme_list.append(t.theme)
info["themes"]=theme_list
url="templates/list_members.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{'info':info})
self.response.out.write(content)
可見它會渲染 templates 下的 list_members.htm 這個網頁模板, 並且將全部主題布景選項放在字典物件中傳遞給它, 以便在新增使用者頁面中產生下拉式選單. 此模板 list_members.htm 內容如下 :
{% extends "jqueryeasyui.htm" %}
{% block style %}
body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
<!--使用者 Members 列表-->
<table id="members" title="使用者列表" style="width:auto" data-options="tools:'#members_tools'"></table>
<div id="members_tools">
<a href="#" id="add_member" class="icon-add" title="新增"></a>
<a href="#" id="edit_member" class="icon-edit" title="編輯"></a>
<a href="#" id="remove_member" class="icon-remove" title="刪除"></a>
<a href="#" id="reload_members" class="icon-reload" title="重新載入"></a>
</div>
<!--新增&編輯 Members 表單對話框-->
<div id="member_dialog" class="easyui-dialog" title="新增連結" style="width:360px;height:270px;" data-options="closed:'true',buttons:'#member_buttons'">
<form id="member_form" method="post" style="padding:10px">
<div style="margin:5px">
<label style="width:60px;display:inline-block;">帳號 : </label>
<input id="member_account" name="account" type="text" class="easyui-textbox" data-options="missingMessage:'此欄位必須為英數字組合',required:true,readonly:false" style="width:230px">
</div>
<div style="margin:5px">
<label style="width:60px;display:inline-block;">密碼 : </label>
<input name="password" type="text" class="easyui-textbox" data-options="missingMessage:'此欄位為必填',required:true" style="width:230px">
</div>
<div style="margin:5px">
<label style="width:60px;display:inline-block;">主題布景 : </label>
<select name="theme" id="user_theme" class="easyui-combobox" data-options="required:true,panelHeight:'auto'">
{% for t in info.themes %}
<option value="{{t}}">{{t}}</option>
{% endfor %}
</select>
</div>
<div style="margin:5px">
<label style="width:60px;display:inline-block;">管理員 : </label>
<select name="is_admin" id="is_admin" class="easyui-combobox" data-options="required:true,panelHeight:'auto'">
<option value="True">True</option>
<option value="False">False</option>
</select>
</div>
<div style="margin:5px">
<label style="width:60px;display:inline-block;">Email : </label>
<input name="email" id="email" type="text" class="easyui-textbox" data-options="required:true" style="width:230px">
</div>
<div style="margin:5px">
<label style="width:60px;display:inline-block;">行動電話 : </label>
<input name="mobile" type="text" class="easyui-textbox" style="width:230px">
<input type="hidden" id="member_op" value="">
</div>
</form>
</div>
<div id="member_buttons" style="padding-right:15px;">
<a href="#" id="clear_member" class="easyui-linkbutton" iconCls="icon-clear" style="width:90px">重設</a>
<a href="#" id="save_member" class="easyui-linkbutton" iconCls="icon-ok" style="width:90px">確定</a>
</div>
<script>
$(function(){
//標頭連結 members
$('#members').datagrid({
columns:[[
{field:'account',title:'帳號',sortable:true},
{field:'password',title:'密碼',sortable:true},
{field:'theme',title:'主題布景',sortable:true},
{field:'is_admin',title:'管理員',sortable:true},
{field:'email',title:'Email',sortable:true},
{field:'mobile',title:'行動電話',sortable:true}
]],
url:"/get_members",
method:"post",
singleSelect:true,
rownumbers:true
});
$("#clear_member").bind("click",function(){
$("#member_form")[0].reset();
});
$("#save_member").bind("click",function(){
var op=$("#member_op").val(); //判斷是新增或修改
if (op=="update") {var url="/update_member";}
else {var url="/add_member";}
$("#member_form").form("submit",{
url:url,
method:"post",
success:function(data){
var data=eval('(' + data + ')');
$("#member_dialog").dialog("close");
if (data.status==="success") {
$("#members").datagrid("reload");
}
else {
$.messager.alert("訊息",data.reason,"error");
}
}
});
});
$("#add_member").bind("click",function(){
$("#member_dialog").dialog("open").dialog("setTitle","新增使用者");
$("#member_account").textbox({"readonly":false}); //for adding
$("#member_form").form("clear");
$("#member_op").val("add");
$('#user_theme').combobox();
$('#user_theme').combobox('setValue','default');
$('#is_admin').combobox();
$('#is_admin').combobox('setValue','False');
$('#email').textbox('setValue','foo@bar.com');
});
$("#edit_member").bind("click",function(){
var row=$("#members").datagrid("getSelected");
if (row) {
$("#member_dialog").dialog("open").dialog("setTitle","編輯使用者");
$("#member_form").form("load",row);
$("#member_account").textbox({"readonly":true});
$("#member_op").val("update");
}
else {$.messager.alert("訊息","請先選取要編輯的使用者!","error");}
});
$("#remove_member").bind("click",function(){
var row=$("#members").datagrid("getSelected");
if (row) {
var params={account:row.account};
$.messager.confirm("確認","確定要刪除這個使用者嗎?",function(btn){
if (btn){
if (row.account=="admin") {
$.messager.alert("訊息","此為系統管理者不可刪除!","error");
return;
}
var callback=function(data){
if (data.status==="success"){
$("#members").datagrid("reload");
}
else {$.messager.alert("訊息",data.reason,"error");}
};
$.post("/remove_member",params,callback,"json");
}
})
}
});
$("#reload_members").bind("click",function(){
$("#members").datagrid("load");
});
});
</script>
{% endblock%}
這裡黃顏色的部分是今天花比較多時間磨合的地方. email 這個欄位很奇怪, 我明明在 model.py 中已經用 required=False 指名此欄位不必須填值, 但測試新增使用者時卻還是報錯, 說 email 不可 empty. 沒辦法, 乾脆在 data-options 中將 email 設為必填. 既然如此, 為了用 Easyui 的 setValue 設定初始值, 就幫此欄位多設一個 id 屬性.
另外, 要幫 theme 與 is_admin 這兩個下拉式選單設定初始值時發現, 直接用 combobox 的 setValue 方法是不行的, 會說找不到 options 物件, 必須先呼叫 combobox() 方法初始化物件, 再呼叫 setValue 方法才會生效.
值得一提的是, 我利用一個隱藏欄位 member_op 來儲存是要 add 還是 update 使用者資料, 如果是編輯動作, 就將 account 欄位設為唯讀 (帳號一經建立不能修改, 只能刪除), 並將 member_op 設為 update; 若為新增動作, 就清除 account 欄位的唯讀屬性, 並將 member_op 設為 add, 這樣就可以共用一張表單了.
此 datagrid 是從 /get_members 路徑取得資料來源, 其路徑處理類別如下 :
class get_members(webapp2.RequestHandler):
def post(self):
page=self.request.get("page")
rows=self.request.get("rows")
sort=self.request.get("sort")
order=self.request.get("order")
if len(page):
page=int(page)
else:
page=1
if len(rows):
rows=int(rows)
else:
rows=10
if not len(sort):
sort="account"
if not len(order):
order="asc"
query=m.Members.gql("ORDER BY %s %s" % (sort, order))
count=query.count()
mbs=query.fetch(rows, (page-1)*rows)
rows=[] #for storing objects
for mb in mbs:
member={"account":mb.account,
"password":mb.password,
"theme":mb.theme,
"is_admin":mb.is_admin,
"email":mb.email,
"mobile":mb.mobile}
rows.append(member)
obj={"total":count,"rows":rows} #Easyui datagrid json format
self.response.headers["Content-Type"]="application/json"
self.response.out.write(json.dumps(obj))
就是讀取全部 Members 資料實體, 將各欄位值編成 json 傳回前端而已.
新增使用者 /add_member 的路徑處理類別如下 :
class add_member(webapp2.RequestHandler):
def post(self):
account=self.request.get("account")
password=self.request.get("password")
theme=self.request.get("theme")
is_admin=self.request.get("is_admin")
email=self.request.get("email", default_value="foo@bar.com")
mobile=self.request.get("mobile", default_value="0933123456")
#trans string to boolean
if is_admin=="True":
is_admin=True
else:
is_admin=False
#check entity if exist
mb=m.Members.get_by_key_name(account)
if mb: #already exist
result='{"status":"failure","reason":"帳號已存在!"}'
else: #new member
member=m.Members(key_name=account,
account=account,
password=password,
theme=theme,
is_admin=is_admin,
email=email,
mobile=mobile
)
member.put()
result='{"status":"success"}'
self.response.out.write(result)
這裡要注意的是資料類型為 boolean 的 is_admin 欄位, 因為前端傳出的 True 與 False 都是字串, 必須轉換為 boolean 欄位才行, 否則會出現資料不符錯誤.
更新 /update_member 與移除 /remove_member 的處理類別如下 :
class update_member(webapp2.RequestHandler):
def post(self):
account=self.request.get("account")
password=self.request.get("password")
theme=self.request.get("theme")
is_admin=self.request.get("is_admin")
email=self.request.get("email")
mobile=self.request.get("mobile", default_value="")
#trans string to boolean
if is_admin=="True":
is_admin=True
else:
is_admin=False
#get entity from store
mb=m.Members.get_by_key_name(account)
if mb: #entity exist
mb.account=account
mb.password=password
mb.theme=theme
mb.is_admin=is_admin
mb.email=email
mb.mobile=mobile
mb.put()
result='{"status":"success"}'
else: #member not existed
result='{"status":"failure","reason":"使用者不存在!"}'
self.response.out.write(result)
class remove_member(webapp2.RequestHandler):
def post(self):
account=self.request.get("account")
#get entity from store
mb=m.Members.get_by_key_name(account)
if mb: #entity exist
db.delete(mb)
result='{"status":"success"}'
else: #member not existed
result='{"status":"failure","reason":"使用者不存在!"}'
self.response.out.write(result)
以上便是整個使用者管理的完整 CRUD 實作紀錄, 實際測試範例如下 (登入帳密 admin, admin 或 guest, guest) :