完成整合訪客紀錄器後, 接下來要處理版面右上方 (north) 的標頭連結, 這裡主要是放置首頁, 登出, 應用程式入口, 或者重要連結的地方. 到目前為止這部分在 main_1, main_2, 到 main_3 都是固定寫好的, 本篇測試是要將其改為可線上更改的 (但首頁與登出除外, 這兩個是固定不可改的).
本測試是在上一篇的基礎上進行修改添加的, 參考 :
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (六) : EasyUI CMS on GAE 之 2
我首先在 model.py 中添加 Headerlinks 資料模型以儲存標頭連結資訊 :
class Headerlinks(db.Model):
name=db.StringProperty()
title=db.StringProperty()
url=db.StringProperty()
target=db.StringProperty()
sequence=db.IntegerProperty()
hint=db.StringProperty()
headerlink=Headerlinks(key_name="home",name="home",title=u"首頁",
url="javascript:gohome()",target="_self",sequence=0,hint=u"首頁")
headerlink.put()
headerlink=Headerlinks(key_name="logout",name="logout",title=u"登出",
url="javascript:logout()",target="_self",sequence=0,hint=u"登出")
headerlink.put()
這裡預設建立首頁與登出這兩個系統連結. 為了區隔前一次測試, 我在 main.py 中新增一個 main_4 類別來渲染新的主頁 main_4.htm :
class main_4(BaseHandler):
def get(self):
#save visitor info
ip=self.request.remote_addr
#user_agent=os.environ.get("HTTP_USER_AGENT")
user_agent=self.request.headers.get("User-Agent")
visitor=m.Visitors()
visitor.ip=ip
visitor.visit_time=datetime.datetime.now() + timedelta(hours=+8)
visitor.user_agent=user_agent
visitor.ip_list=list(ip)
visitor.user_agent_list=list(user_agent)
visitor.put()
#check login session
info={} #for storing parameters
account=self.session.get('account')
if account: #already logged in
info["account"]=account
s=m.Settings.get_by_key_name("settings")
info["site_title"]=s.site_title
theme=self.session.get('theme')
info["theme"]=theme
#create param: greeting
today=datetime.date.today()
week=[u"一",u"二",u"三",u"四",u"五",u"六",u"日"]
info["greeting"]=u"您好! " + account + u", 今天是 " + \
str(today.year) + u" 年 " + str(today.month) + u" 月 " + \
str(today.day) + u" 日 星期" + week[today.weekday()]
#create param: themes
theme_list=[]
themes=m.Themes.all()
for t in themes:
theme_list.append(t.theme)
info["themes"]=theme_list
#create param: headerlinks
headerlinks=m.Headerlinks.all()
headerlinks.order("-sequence") #sort by sequence (reverse)
link_list=[] #for storing headerlinks objects
for h in headerlinks:
link={}
link["title"]=h.title
link["url"]=h.url
link["target"]=h.target
link["sequence"]=h.sequence
link["hint"]=h.hint
link_list.append(link)
info["headerlinks"]=link_list
url="templates/main_4.htm"
else: #not logged in
s=m.Settings.get_by_key_name("settings")
info["site_title"]=s.site_title
info["site_theme"]=s.site_theme
url="templates/login.htm"
theme=self.session.get('theme')
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{'info':info})
self.response.out.write(content)
上面藍色部分是為了標頭連結新增的部分, 主要是從 Datastore 讀取 Headerlinks 模型的資料實體, 然後將資訊存入字典, 一一放入串列中, 然後放在 info 物件的 headerlinks 參數中傳給 main_4.htm 網頁.
接著將上一篇文章的主版面檔案 main_3.htm 複製到 main_4.htm, 修改如下 :
{% extends "jqueryeasyui.htm" %}
{% block style %}
a {text-decoration:none;}
a:hover {text-decoration:underline;background-color:yellow;}
#west {width:150px;}
#west-inner {border-top:0px;border-right:0px;border-bottom:0px;}
.nav {padding:5px;}
.tab {padding:10px;}
#north {height:55px;overflow:hidden;}
#north-table {width:100%;border-spacing:0px}
#north-left {text-align:left;padding:5px;}
#north-right {text-align:right;padding:5px;}
{% endblock%}
{% block body %}
<div id="north" title="{{ info.site_title }}" data-options="region:'north',border:false,collapsible:true,tools:'#tools'">
<form id="header-form" method="post">
<table id="north-table">
<tr>
<td id="north-left" style="vertical-align:middle">
{{ info.greeting }}
</td>
<td id="north-right" style="vertical-align:middle">
<span id="header_links">
{% for h in info.headerlinks %}
<a href="{{h.url}}" target="{{h.target}}" title="{{h.hint}}">{{h.title}}</a>.
{% endfor %}
</span>
<select id="theme_sel" name="theme" class="easyui-combobox" style="width:120px;height:18px" panelHeight="auto">
<option value="default">主題布景</option>
{% for t in info.themes %}
<option value="{{t}}"{% ifequal t info.theme %} selected{% endifequal %}>{{t}}</option>
{% endfor %}
</select>
</td>
</tr>
</table>
</form>
</div>
<div id="tools">
<a href="javascript:logout()" class="icon-remove" title="登出"></a>
</div>
<div title="導覽" data-options="region:'west',border:true" id="west">
<div class="easyui-accordion" id="west-inner">
<div title="主功能" class="nav">
.<a href="javascript:gohome()" target="_self" title="首頁">首頁</a><br>
.<a href="javascript:logout()" target="_self" title="登出">登出</a><br>
</div>
</div>
</div>
<div data-options="region:'center',border:false,href:'/systabs'" id="center">
</div>
<script>
$("body").attr("class", "easyui-layout");
$(document).ready(function(){
//var css="http://www.jeasyui.com/easyui/themes/{{ info.theme }}/easyui.css";
var css="/static/easyui/themes/{{ info.theme }}/easyui.css";
$("#theme").attr("href", css);
$("#theme_sel").combobox({
onSelect:function(rec){
//var css="http://www.jeasyui.com/easyui/themes/" + rec.value + "/easyui.css";
var css="/static/easyui/themes/" + rec.value + "/easyui.css";
$("#theme").attr("href", css);
$.get("/change_theme",{theme:rec.value});
}
});
});
<!-- 系統的載入與登出函式 -->
function gohome(){
$(function(){
var p=$("body").layout("panel","center");
p.panel({href:"/systabs"});
});
}
function logout(){
$(function(){
$.messager.confirm("確認","確定要登出系統嗎?",function(btn){
if (btn) {window.location.href="/logout";}
});
});
}
</script>
{% endblock%}
上面主要的修改是藍色的部分, 將固定的網頁改為從 main.py 傳來的 info.headerlinks 物件製作超連結.
此外也在 model.py 中的系統頁籤資料表 Systabs 添加一個資料實體 list_headerlinks, 用來管理標頭超連結 :
systab=Systabs(key_name="list_headerlinks",tab_name="list_headerlinks",
tab_label=u"標頭連結",tab_link="/list_headerlinks",tab_order=3,tab_admin=True)
systab.put()
這個 list_headerlinks 路徑在 main.py 裡的處理類別如下 :
class list_headerlinks(webapp2.RequestHandler):
def get(self):
url="templates/list_headerlinks.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
它只是簡單地渲染 list_headerlinks.htm 這個網頁而已, 其內容為 :
{% extends "jqueryeasyui.htm" %}
{% block style %}
body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
<!--標頭連結 sys_header_links 列表-->
<table id="sys_header_links" title="標頭連結列表" style="width:auto" data-options="tools:'#header_links_tools'"></table>
<div id="header_links_tools">
<a href="#" id="add_header_link" class="icon-add" title="新增"></a>
<a href="#" id="edit_header_link" class="icon-edit" title="編輯"></a>
<a href="#" id="remove_header_link" class="icon-remove" title="刪除"></a>
<a href="#" id="reload_header_links" class="icon-reload" title="重新載入"></a>
</div>
<!--新增&編輯 Headerlinks 表單對話框-->
<div id="header_link_dialog" class="easyui-dialog" title="新增使用者" style="width:360px;height:270px;" data-options="closed:'true',buttons:'#header_link_buttons'">
<form id="header_link_form" method="post" style="padding:10px">
<div style="margin:5px">
<label style="width:60px;display:inline-block;">名稱 : </label>
<input name="name" id="link_name" 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="title" 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>
<input name="url" 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="target" class="easyui-combobox" data-options="panelHeight:'auto'">
<option value="_self">_self</option>
<option value="_blank">_blank</option>
</select>
</div>
<div style="margin:5px">
<label style="width:60px;display:inline-block;">順序 : </label>
<input id="header_link_sequence" name="sequence">
</div>
<div style="margin:5px">
<label style="width:60px;display:inline-block;">提示 : </label>
<input name="hint" type="text" class="easyui-textbox" style="width:230px">
<input type="hidden" id="header_link_op" value="">
</div>
</form>
</div>
<div id="header_link_buttons" style="padding-right:15px;">
<a href="#" id="clear_header_link" class="easyui-linkbutton" iconCls="icon-clear" style="width:90px">重設</a>
<a href="#" id="save_header_link" class="easyui-linkbutton" iconCls="icon-ok" style="width:90px">確定</a>
</div>
<script>
$(function(){
//標頭連結 sys_header_links
$('#sys_header_links').datagrid({
columns:[[
{field:'name',title:'name',sortable:true},
{field:'title',title:'title',sortable:true},
{field:'url',title:'url',sortable:true},
{field:'target',title:'target',sortable:true},
{field:'sequence',title:'sequence',align:'center',sortable:true},
{field:'hint',title:'hint',sortable:true}
]],
url:"/get_headerlinks",
method:"post",
fitColumns:true,
singleSelect:true,
rownumbers:true
});
$("#header_link_sequence").numberspinner({
min:0,
max:99,
increment:1,
value:"0",
required:true,
missingMessage:'此欄位為必填'
});
$("#clear_header_link").bind("click",function(){
$("#header_link_form")[0].reset();
});
$("#save_header_link").bind("click",function(){
var op=$("#header_link_op").val(); //判斷是新增或修改
if (op=="update") {
var row=$("#sys_header_links").datagrid("getSelected");
if (row.name=="home" || row.name=="logout") {
$("#header_link_dialog").dialog('close');
$.messager.alert("訊息","此為系統連結不可更改!","error");
return;
}
var url="/update_headerlink";
}
else {var url="/add_headerlink";}
$("#header_link_form").form("submit",{
url:url,
method:"post",
success:function(data){
var data=eval('(' + data + ')');
$("#header_link_dialog").dialog("close");
if (data.status==="success") {
$("#sys_header_links").datagrid("reload");
}
else {
$.messager.alert("訊息",data.reason,"error");
}
}
});
});
$("#add_header_link").bind("click",function(){
$("#header_link_dialog").dialog("open").dialog("setTitle","新增連結");
$("#link_name").textbox({"readonly":false}); //for adding
$("#header_link_form").form("clear");
$("#header_link_op").val("add");
});
$("#edit_header_link").bind("click",function(){
var row=$("#sys_header_links").datagrid("getSelected");
if (row) {
$("#header_link_dialog").dialog("open").dialog("setTitle","編輯連結");
$("#header_link_form").form("load",row);
$("#link_name").textbox({"readonly":true});
$("#header_link_op").val("update");
}
else {$.messager.alert("訊息","請先選取要編輯的連結!","error");}
});
$("#remove_header_link").bind("click",function(){
var row=$("#sys_header_links").datagrid("getSelected");
if (row) {
var params={name:row.name};
$.messager.confirm("確認","確定要刪除這個連結嗎?",function(btn){
if (btn){
if (row.title=="首頁" || row.title=="登出") {
$.messager.alert("訊息","此為系統連結不可刪除!","error");
return;
}
var callback=function(data){
if (data.status==="success"){
$("#sys_header_links").datagrid("reload");
}
else {$.messager.alert("訊息",data.reason,"error");}
};
$.post("/remove_headerlink",params,callback,"json");
}
})
}
});
$("#reload_header_links").bind("click",function(){
$("#sys_header_links").datagrid("load");
});
});
</script>
{% endblock%}
這個 list_headerlinks.htm 主要是以 Datagrid 顯示資料表 Headerlinks 的內容, 但做法與 PHP 版的有些不一樣, 因為 PHP 版可以用 Primary Key id 辨識每一筆紀錄, 而 GAE 的 Datastore 則須使用 key_name, 因此我新增了一個 name 欄位來當作 key 的來源. 另外就是 PHP 版用 op 參數來區別 sys.php 中的程式, 而在 GAE 則直接使用類別名稱, 做法不同. 這裡我保留了隱藏欄位 header_link_op 用來記錄是新增 (add) 還是編輯更新 (update) 動作, 因為這兩種操作共用一個 Dialog 對話框元件, 當按下右上角的新增鈕時, 就將此隱藏欄位設為 add; 按下編輯鈕時就設為 update, 紀錄動作狀態主要目的是在按下確定鈕時, 程式可以設定 Ajax 要向哪一個 url 提出要求.
還有一個重點是, 因為在 GAE 是使用 key_name (=name) 欄位來取代 MySQL 的自動增量主鍵 id, 因此編輯時 name 欄位 readonly 屬性必須設為 false, 而新增時設為 true.
另外, 雖然我在表單的提交 (submit) 處理中有指定 method 為 post, 但不知何故無效, 表單仍以 get 方法提交, 導致後端錯誤 (因為 main.py 中只實作 post 方法). 我只好在 form 元素中添加 method="post" 解決.
上面這個顯示 Headerlinks 的網頁是以 Ajax 方式向後端的 /get_headerlinks 取得 json 資料, 此路徑之處理類別為 :
class get_headerlinks(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="sequence"
if not len(order):
order="asc"
query=m.Headerlinks.gql("ORDER BY %s %s" % (sort, order))
count=query.count()
links=query.fetch(rows, (page-1)*rows)
rows=[] #for storing objects
for h in links:
link={"name":h.name,
"title":h.title,
"url":h.url,
"target":h.target,
"sequence":h.sequence,
"hint":h.hint}
rows.append(link)
obj={"total":count,"rows":rows} #Easyui datagrid json format
self.response.headers["Content-Type"]="application/json"
self.response.out.write(json.dumps(obj))
這裡的重點是利用串列 rows 來儲存從 Headerlinks 資料表裡擷取出來的每一筆資料實體 (放在字典物件中), 然後轉成 json 格式傳回前端. 另外新增, 更新, 刪除三個動作會向後端提出要求, 其路徑處理類別如下 :
class add_headerlink(webapp2.RequestHandler):
def post(self):
name=self.request.get("name")
#check entity if exist
link=m.Headerlinks.get_by_key_name(name)
if link: #already exist
result='{"status":"failure","reason":"連結名稱已存在!"}'
else: #new link
headerlink=m.Headerlinks(key_name=name,name=name,
title=self.request.get("title"),
url=self.request.get("url"),
target=self.request.get("target"),
sequence=int(self.request.get("sequence")),
hint=self.request.get("hint")
)
headerlink.put()
result='{"status":"success"}'
self.response.out.write(result)
這裡先取得前端傳出的 name 欄位值, 然後以此為 key 到 Datastore 中搜尋看看是否已經有相同名稱的資料實體, 有的話表示同名連結已存在, 回應錯誤狀態與訊息; 沒有的話就新增一個資料實體. 參考 :
# Check If Entity in Datastore exists in GAE Python
我原先用下列做法新增資料實體, 結果其他欄位都沒問題, 唯獨 key_name 欄位根本沒寫進去 :
headerlink=m.Headerlinks()
headerlink.key_name=name
headerlink.name=name
headerlink.title=self.request.get("title")
headerlink.url=self.request.get("url")
headerlink.target=self.request.get("target")
headerlink.sequence=int(self.request.get("sequence"))
headerlink.hint=self.request.get("hint")
headerlink.put()
改成前面那個寫法就可以了. 書上兩種寫法都可以, 但使用 key_name 欄位時上面這個寫法就不行了, 這是要特別注意的地方. 更新操作的路徑處理類別如下 :
class update_headerlink(webapp2.RequestHandler):
def post(self):
name=self.request.get("name")
#get entity from store
link=m.Headerlinks.get_by_key_name(name)
if link: #entity exist
link.title=self.request.get("title")
link.url=self.request.get("url")
link.target=self.request.get("target")
link.sequence=int(self.request.get("sequence"))
link.hint=self.request.get("hint")
link.put()
result='{"status":"success"}'
else: #link not existed
result='{"status":"failure","reason":"連結不存在!"}'
self.response.out.write(result)
同樣也是先去查詢此 key_name 是否存在, 但與 add 相反, 存在的話更新資料實體內容再回存, 不存在就回應錯誤訊息. 而刪除操作的類別為 :
class remove_headerlink(webapp2.RequestHandler):
def post(self):
name=self.request.get("name")
#get entity from store
link=m.Headerlinks.get_by_key_name(name)
if link: #entity exist
db.delete(link)
result='{"status":"success"}'
else: #link not existed
result='{"status":"failure","reason":"連結不存在!"}'
self.response.out.write(result)
實際測試範例與 zip 原始檔如下 :
測試 4 : http://jqueryeasyui.appspot.com/main_4 (下載原始碼) (備用下載點)
OK, 終於搞定了, 這是個比較完整的測試, 包含了 GAE 上全部的 CRUD (增讀修刪) 作業. 有了這組模板, 其他的功能實作起來就會比較快了.
沒有留言:
張貼留言