完成整合訪客紀錄器後, 接下來要處理版面右上方 (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 (增讀修刪) 作業. 有了這組模板, 其他的功能實作起來就會比較快了.
2016年2月4日 星期四
2016年2月3日 星期三
看電影 : 福爾摩斯地獄新娘
因累積的補休已達八小時, 且本月有一個小時必須休掉, 下午就請了一個小時補休帶小狐狸們去美奇萊看 "新世紀福爾摩斯 : 地獄新娘". 這三年元旦我們都會去那裏看電影, 但今年卻不知怎麼忘了, 趁著春節前的寒假空檔, 正好有姊姊最喜歡的福爾摩斯電影 (她是康柏拜區迷), 而且剛好他們三個都有空, 昨天就敲定看這部地獄新娘.
老實說, 這部片我看不太懂, 因為一下子是現代, 一下子又切換到 1895 年的倫敦, 我都被搞混了. 出來後我問二哥看得懂嗎? 他說還可以. 還是姐姐之前有看電視劇集的看得懂它在演啥. 原來這一切都是福爾摩斯的幻想辦案, 解開了 120 年前地獄新娘的謎團.
# [Sherlock] 新世紀福爾摩斯:地獄新娘~是影集不是電影唷!~
我向來對推理劇不太有興趣, 進電影院我要嘛看喜劇, 要嘛看動作片, 推理劇太耗腦力了. 我看不看得懂不重要, 其實我主要是讓小狐狸們有一個共同的活動, 因為過去幾年因為媽生病的關係, 很少帶他們出去玩 (只能利用學校的戶外教學). 幾年後如果各自外出求學, 聚在一起的機會就少了.
人生就是共同生活的記憶, 這是我這幾天找到的第一個答案.
老實說, 這部片我看不太懂, 因為一下子是現代, 一下子又切換到 1895 年的倫敦, 我都被搞混了. 出來後我問二哥看得懂嗎? 他說還可以. 還是姐姐之前有看電視劇集的看得懂它在演啥. 原來這一切都是福爾摩斯的幻想辦案, 解開了 120 年前地獄新娘的謎團.
# [Sherlock] 新世紀福爾摩斯:地獄新娘~是影集不是電影唷!~
我向來對推理劇不太有興趣, 進電影院我要嘛看喜劇, 要嘛看動作片, 推理劇太耗腦力了. 我看不看得懂不重要, 其實我主要是讓小狐狸們有一個共同的活動, 因為過去幾年因為媽生病的關係, 很少帶他們出去玩 (只能利用學校的戶外教學). 幾年後如果各自外出求學, 聚在一起的機會就少了.
人生就是共同生活的記憶, 這是我這幾天找到的第一個答案.
2016年2月2日 星期二
如何在 GAE 上佈署 jQuery EasyUI 專案 (七) : EasyUI CMS on GAE 之 2
完成 EasyUI CMS on GAE Part 1 測試後, 繼續來給它添柴火. 首先我想把之前寫的訪客紀錄器整合進來, 參考 :
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (五) : 紀錄到訪者
我先在 model.py 中給 systabs 增加一個只有管理者能看到的系統頁籤實體 :
systab=Systabs(key_name="list_visitors",tab_name="list_visitors",
tab_label=u"到訪者",tab_link="/list_visitors",tab_order=2,tab_admin=True)
systab.put()
為了與前一篇的測試程式區隔, 我在 main.py 中增加新的路徑 main_3, 其處理類別則從前一篇的 main_2 複製過來, 修改渲染對象為 main_3.htm, 並在前面加上紀錄訪客資訊的程式碼, 如下藍色的部分所示 :
class main_3(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
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()]
theme_list=[]
themes=m.Themes.all()
#themes.order("theme")
for t in themes:
theme_list.append(t.theme)
info["themes"]=theme_list
url="templates/main_3.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)
所渲染之網頁模板 main_3.htm 也是從 main_2.htm 複製而來, 只是把其中的 systabs_2 改為 systabs_3 而已 :
{% 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">
<a href="javascript:gohome()" target="_self" title="首頁">首頁</a>.
<a href="javascript:logout()" target="_self" title="登出">登出</a>.
</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_3'" 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_3"});
});
}
function logout(){
$(function(){
$.messager.confirm("確認","確定要登出系統嗎?",function(btn){
if (btn) {window.location.href="/logout";}
});
});
}
</script>
{% endblock%}
系統頁籤路徑 systabs_3 的處理類別也是從 systabs_2 複製而得, 僅修改所渲染的網頁為 systabs_3.htm, 如下所示 :
class systabs_3(BaseHandler):
def get(self):
systabs=m.Systabs.all()
systabs.order("tab_order") #sort by tab_order
tabs=[] #for storing tab objects
is_admin=self.session.get('is_admin') #True/False
for t in systabs:
tab={}
tab["tab_label"]=t.tab_label
tab["tab_link"]=t.tab_link
if t.tab_admin: #this tab is for admin only
if is_admin: #current user is admin
tabs.append(tab)
else: #this tab is for registered users
tabs.append(tab)
url="templates/systabs_3.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{"tabs":tabs})
self.response.out.write(content)
所渲染的系統頁籤網頁 systabs_3.htm 則從 systabs_2.htm 複製而來, 因為邏輯相同, 完全不用改 :
<div id="sys_tabs" class="easyui-tabs" data-options="fit:'true'">
{% for t in tabs %}
<div class="tab" title="{{t.tab_label}}" data-options="href:'{{t.tab_link}}',loadingMessage:'載入中 ... '"></div>
{% endfor %}
</div>
這樣顯示的部分就搞定了, 接下來處理新增的訪客頁籤路徑 /list_visitors, 其處理類別如下, 就是渲染 list_visitors.htm 網頁 :
class list_visitors(webapp2.RequestHandler):
def get(self):
url="templates/list_visitors.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
而網頁 list_visitors.htm 則是從下面這篇文章的測試 6 的 list_visitors_6.htm 複製過來修改的 :
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (五) : 紀錄到訪者
測試 3 : http://jqueryeasyui.appspot.com/main_3 (下載原始碼) (備用下載點)
現在只是測試, 以後其實可以考慮在 Visitors 設 key_name, 以 IP 位址加上格式化時間當 key, 這樣刪除時就可以比較精確了. 另外, 日期搜尋似乎有問題, 不是很重要, 先擱著, 有空再 debug.
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (五) : 紀錄到訪者
我先在 model.py 中給 systabs 增加一個只有管理者能看到的系統頁籤實體 :
systab=Systabs(key_name="list_visitors",tab_name="list_visitors",
tab_label=u"到訪者",tab_link="/list_visitors",tab_order=2,tab_admin=True)
systab.put()
為了與前一篇的測試程式區隔, 我在 main.py 中增加新的路徑 main_3, 其處理類別則從前一篇的 main_2 複製過來, 修改渲染對象為 main_3.htm, 並在前面加上紀錄訪客資訊的程式碼, 如下藍色的部分所示 :
class main_3(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
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()]
theme_list=[]
themes=m.Themes.all()
#themes.order("theme")
for t in themes:
theme_list.append(t.theme)
info["themes"]=theme_list
url="templates/main_3.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)
所渲染之網頁模板 main_3.htm 也是從 main_2.htm 複製而來, 只是把其中的 systabs_2 改為 systabs_3 而已 :
{% 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">
<a href="javascript:gohome()" target="_self" title="首頁">首頁</a>.
<a href="javascript:logout()" target="_self" title="登出">登出</a>.
</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_3'" 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_3"});
});
}
function logout(){
$(function(){
$.messager.confirm("確認","確定要登出系統嗎?",function(btn){
if (btn) {window.location.href="/logout";}
});
});
}
</script>
{% endblock%}
系統頁籤路徑 systabs_3 的處理類別也是從 systabs_2 複製而得, 僅修改所渲染的網頁為 systabs_3.htm, 如下所示 :
class systabs_3(BaseHandler):
def get(self):
systabs=m.Systabs.all()
systabs.order("tab_order") #sort by tab_order
tabs=[] #for storing tab objects
is_admin=self.session.get('is_admin') #True/False
for t in systabs:
tab={}
tab["tab_label"]=t.tab_label
tab["tab_link"]=t.tab_link
if t.tab_admin: #this tab is for admin only
if is_admin: #current user is admin
tabs.append(tab)
else: #this tab is for registered users
tabs.append(tab)
url="templates/systabs_3.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{"tabs":tabs})
self.response.out.write(content)
所渲染的系統頁籤網頁 systabs_3.htm 則從 systabs_2.htm 複製而來, 因為邏輯相同, 完全不用改 :
<div id="sys_tabs" class="easyui-tabs" data-options="fit:'true'">
{% for t in tabs %}
<div class="tab" title="{{t.tab_label}}" data-options="href:'{{t.tab_link}}',loadingMessage:'載入中 ... '"></div>
{% endfor %}
</div>
這樣顯示的部分就搞定了, 接下來處理新增的訪客頁籤路徑 /list_visitors, 其處理類別如下, 就是渲染 list_visitors.htm 網頁 :
class list_visitors(webapp2.RequestHandler):
def get(self):
url="templates/list_visitors.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
而網頁 list_visitors.htm 則是從下面這篇文章的測試 6 的 list_visitors_6.htm 複製過來修改的 :
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (五) : 紀錄到訪者
{% extends "jqueryeasyui.htm" %}
{% block style %}
body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
<table id="visitors" title="訪客列表" style="width:auto;" data-options="tools:'#visitors_tools',toolbar:'#visitors_search_bar'"></table>
<div id="visitors_tools">
<a href="#" id="remove_visitor" class="icon-remove" title="刪除"></a>
<a href="#" id="reload_visitors" class="icon-reload" title="重新載入"></a>
</div>
<div id="visitors_search_bar" style="text-align:right;padding:2px;">
<select id="visitors_search_field" class="easyui-combobox" data-options="panelHeight:'auto'">
<option value="ip">IP 位址</option>
<option value="visit_time">到訪時間</option>
</select>
<input id="visitors_search_what" class="easyui-textbox">
</div>
<script language="javascript">
$(document).ready(function(){
$('#visitors').datagrid({
url:'/get_visitors',
method:"post",
columns:[[
{field:'ip',title:'IP 位址',sortable:true},
{field:'visit_time',title:'到訪時間',sortable:true},
{field:'user_agent',title:'瀏覽器',sortable:true}
]],
singleSelect:true,
fitColumns:true,
collapsible:true,
rownumbers:true,
pagination:true,
collapsible:false,
pageSize:10
});
$("#visitors_search_what").textbox({
icons:[{
iconCls:"icon-search",
handler:function(e){
$('#visitors').datagrid("load",{
search_field:$("#visitors_search_field").combobox("getValue"),
search_what:$("#visitors_search_what").textbox("getValue")
});
}
}]
});
$("#reload_visitors").bind("click",function(){
$("#visitors").datagrid("load");
});
$("#remove_visitor").bind("click",function(){
var row=$("#visitors").datagrid("getSelected");
if (row) {
$.messager.confirm("確認","確定要刪除這筆到訪紀錄嗎?",function(btn){
if (btn){
var params={ip:row.ip,visit_time:row.visit_time};
var callback=function(data){
if (data.status==="success"){
$("#visitors").datagrid("reload");
}
else {$.messager.alert("訊息","刪除到訪紀錄失敗!","error");}
};
$.post("/remove_visitor",params,callback,"json");
}
})
}
else {$.messager.alert("訊息","請點選要刪除的資料!")}
});
});
</script>
{% endblock%}
這裡用 Ajax 以 post 方法向後端 /get_visitors 取得 datagrid 所需的 json 資料; 點選任一列後按右上角刪除鈕則會用 Ajax 的 post 向後端 /remove_visitor 要求刪除資料. 上面黃色部分是 PHP 版所無, 我覺得加上去會比較好, 下次要改 PHP 版時要加上去.
路徑 get_visitors 的處理類別是從上述舊作的 get_visitors_6 複製過來的, 邏輯相同, 內容完全不用改, 如下所示 :
路徑 get_visitors 的處理類別是從上述舊作的 get_visitors_6 複製過來的, 邏輯相同, 內容完全不用改, 如下所示 :
class get_visitors(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")
search_field=self.request.get("search_field")
search_what=self.request.get("search_what")
if len(page):
page=int(page)
else:
page=1
if len(rows):
rows=int(rows)
else:
rows=10
if not len(sort):
sort="visit_time"
if not len(order):
order="desc"
if len(search_field):
if search_field=="ip_list":
query=m.Visitors.gql("WHERE ip_list = :1", list(search_what))
elif search_field=="user_agent_list":
query=m.Visitors.gql("WHERE user_agent_list = :1", list(search_what))
else:
query=m.Visitors.all()
query.filter(search_field + " >= ", search_what)
query.filter(search_field + " < ", search_what + u'\ufffd')
else:
query=m.Visitors.gql("ORDER BY %s %s" % (sort, order))
visitors=query.fetch(rows, (page-1)*rows)
rows=[]
for v in visitors:
visit_time=v.visit_time.strftime("%Y-%m-%d %H:%M:%S")
visitor={"ip":v.ip,
"visit_time":visit_time,
"user_agent":v.user_agent}
rows.append(visitor)
count=query.count()
obj={"total":count,"rows":rows}
self.response.headers["Content-Type"]="application/json"
self.response.out.write(json.dumps(obj))
而刪除資料的路徑 /remove_visitor 處理類別則是新寫的, 因為之前從沒試過刪除 Datastore 中的資料, 如下所示 :
class remove_visitor(webapp2.RequestHandler):
def post(self):
ip=self.request.get("ip")
visit_time=self.request.get("visit_time")
#dtobj=datetime.datetime.strptime(visit_time,"%Y-%m-%d %H:%M:%S")
query=m.Visitors.all()
query.filter("ip = ",ip)
#query.filter("visit_time >= ",dtobj)
visitor=query.fetch(1,0)
db.delete(visitor)
content='{"status":"success"}'
self.response.out.write(content)
由於 Visitors 資料表的各欄位沒有一個可作為 key, 所以我就沒有設 key name, 這造成要刪除時無法精準鎖定要刪除的資料實體. 雖然 list_visitors.htm 網頁會傳出 ip 與 visit_time 這兩個參數, 但是其中的 visit_time 值是為了顯示方便而格式化的值 (只到秒), 與存在資料庫中的實體的 visit_time 欄位值並不同, 我參考下列這篇將 visit_time 轉為 datetime 物件實體, 用 >= 去過濾, 但卻出現 Server Internal Error! 所以只好僅過濾 ip 欄位, 並用 fetch(1,0) 刪除符合該 ip 的第一筆 (不是時間最早的, 而是搜尋到的第一筆).
# string to datetime with fractional seconds, on Google App Engine
實際測試範例與 zip 原始檔如下 :
# string to datetime with fractional seconds, on Google App Engine
實際測試範例與 zip 原始檔如下 :
測試 3 : http://jqueryeasyui.appspot.com/main_3 (下載原始碼) (備用下載點)
現在只是測試, 以後其實可以考慮在 Visitors 設 key_name, 以 IP 位址加上格式化時間當 key, 這樣刪除時就可以比較精確了. 另外, 日期搜尋似乎有問題, 不是很重要, 先擱著, 有空再 debug.
2016年2月1日 星期一
如何在 GAE 上佈署 jQuery EasyUI 專案 (六) : EasyUI CMS on GAE 之 1
在 1 月的最後一天終於把 EasyUI CMS on GAE 搞定了, 這兩天花了點時間把測試過程紀錄如下, 以資往後查考之用 (否則一個月之後就會漸漸忘記為啥要這樣寫). 這項測試其實是 1/12 開始進行的, 因為那天下午才要去公司, 早上在家繼續玩 GAE 測試, 本來想測試留言板功能, 但是有一股力量拉著我直攻本次臨時起 "義" 的目標~~把 EasyUI CMS 移植到 GAE 上! 完成後再把留言板功能建在 CMS 裡面. 但接下來有資安的公事要處理, 分心研究了 TinyMCE 網頁編輯器, 又被指派去台北簡報, 將近兩個禮拜沒進度, 到 1 月底才又繼續.
在此次測試過程中發現, EasyUI 最近已經升版為 1.4.4, 之前利用 CDN 取得 jQuery 與 EasyUI 資源在 1.4.3 版測試的結果到了 1.4.4 版就會先後出現如下錯誤訊息 :
TypeError: $.data(...) is undefined
TypeError: $.fn.tabs.methods[_37a] is not a function
有可能 EasyUI 有發現這些問題正在修改, 才會先後出現不同錯誤訊息. 為了測試能順利進行 (沒時間去研究新版的變動), 只好自備 1.4.3 版的資源檔在 /static/easyui 下面, 結構如下 :
同時改寫 template/jqueryeasyui.htm 這個模板如下 :
{% extends "html5.htm" %}
{% block link %}
<link id="theme" rel="stylesheet" href="http://www.jeasyui.com/easyui/themes/default/easyui.css">
<link rel="stylesheet" href="http://www.jeasyui.com/easyui/themes/icon.css">
{% endblock %}
{% block javascript %}
<script type="text/javascript" src="http://www.jeasyui.com/easyui/jquery.min.js"></script>
<script type="text/javascript" src="http://www.jeasyui.com/easyui/jquery.easyui.min.js"></script>
<script type="text/javascript" src="http://www.jeasyui.com/easyui/locale/easyui-lang-zh_TW.js"></script>
{% endblock %}
事實上我是先將原來使用 CDN 的 jqueryeasui.htm 更名為 jqueryeasyui_cdn.htm, 然後才去修改的, 改好後又將其複製為 jqueryeasyui_self.htm, 這樣可以方便切換兩種供檔方式. 切換時先將現在使用中的 jqueryeasui.htm 刪除, 然後複製上面兩個來源檔, 改名為 jquerywasyui.htm 即可.
本篇測試是在下面第一篇測試的基礎上添加的 :
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (五) : 紀錄到訪者
在此次測試過程中發現, EasyUI 最近已經升版為 1.4.4, 之前利用 CDN 取得 jQuery 與 EasyUI 資源在 1.4.3 版測試的結果到了 1.4.4 版就會先後出現如下錯誤訊息 :
TypeError: $.data(...) is undefined
TypeError: $.fn.tabs.methods[_37a] is not a function
有可能 EasyUI 有發現這些問題正在修改, 才會先後出現不同錯誤訊息. 為了測試能順利進行 (沒時間去研究新版的變動), 只好自備 1.4.3 版的資源檔在 /static/easyui 下面, 結構如下 :
同時改寫 template/jqueryeasyui.htm 這個模板如下 :
{% extends "html5.htm" %}
{% block link %}
<link id="theme" rel="stylesheet" href="http://www.jeasyui.com/easyui/themes/default/easyui.css">
<link rel="stylesheet" href="http://www.jeasyui.com/easyui/themes/icon.css">
{% endblock %}
{% block javascript %}
<script type="text/javascript" src="http://www.jeasyui.com/easyui/jquery.min.js"></script>
<script type="text/javascript" src="http://www.jeasyui.com/easyui/jquery.easyui.min.js"></script>
<script type="text/javascript" src="http://www.jeasyui.com/easyui/locale/easyui-lang-zh_TW.js"></script>
{% endblock %}
事實上我是先將原來使用 CDN 的 jqueryeasui.htm 更名為 jqueryeasyui_cdn.htm, 然後才去修改的, 改好後又將其複製為 jqueryeasyui_self.htm, 這樣可以方便切換兩種供檔方式. 切換時先將現在使用中的 jqueryeasui.htm 刪除, 然後複製上面兩個來源檔, 改名為 jquerywasyui.htm 即可.
本篇測試是在下面第一篇測試的基礎上添加的 :
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (五) : 紀錄到訪者
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (四) : Google 登入
首先我參考了之前使用 PHP 版的文章先做好 GAE 版的版面配置, 記得當時可是花了一整天才調好自訂的樣式表, 得到了一個滿意的版面, 參考下面這篇 PHP 版的 :
# 用 jQuery EasyUI 打造輕量級 CMS (一)
在 PHP 版的 CMS 裡, 此網頁稱為 main.php, 而在 GAE 是利用 Python 程式的 main.py 去渲染網頁模板, 所以我把 CMS 的網頁模板取名為 main_1.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="EasyUI-based CMS on GAE" 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">
您好! admin, 今天是 2016 年 01 月 12 日 星期二
</td>
<td id="north-right" style="vertical-align:middle">
<span id="header_links">
<a href="javascript:gohome()" target="_self" title="首頁">首頁</a>.
<a href="javascript:logout()" target="_self" title="登出">登出</a>.
</span>
<select id="theme_sel" name="theme" class="easyui-combobox" style="width:120px;height:18px" panelHeight="auto">
<option value="default">主題布景</option>
<option value="default" selected>default</option>
<option value="gray">gray</option>
<option value="black">black</option>
<option value="bootstrap">bootstrap</option>
<option value="metro">metro</option>
<option value="metro-blue">metro-blue</option>
<option value="metro-gray">metro-gray</option>
<option value="metro-green">metro-green</option>
<option value="metro-orange">metro-orange</option>
<option value="metro-red">metro-red</option>
<option value="ui-cupertino">ui-cupertino</option>
<option value="ui-dark-hive">ui-dark-hive</option>
<option value="ui-pepper-grinder">ui-pepper-grinder</option>
<option value="ui-sunny">ui-sunny</option>
</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_1'" id="center">
</div>
<script>
//$(document.body).addClass("easyui-layout");
$("body").attr("class", "easyui-layout");
<!-- 系統的載入與登出函式 -->
function gohome(){
$(function(){
var p=$("body").layout("panel","center");
p.panel({href:"/systabs_1"});
});
}
function logout(){
$(function(){
$.messager.confirm("確認","確定要登出系統嗎?",function(btn){
if (btn) {window.location.href="/logout";}
});
});
}
</script>
{% endblock%}
測試 1 : http://jqueryeasyui.appspot.com/main_1 (下載原始碼) (備用下載點)
在這個網頁模板中, 我們使用 jQuery 的 attr() 方法將 body 元素加上 "easyui-layout" 樣式, 這樣便能利用 div 元素來進行排版. 版面分割為 north, left, 以及 center 三個版面, 上方的 north 用來放置網站標題, 日期與問候語, 應用程式超連結, 以及主題切換器. 左方是導覽列, 中央則是內容區, 用來呈現系統設管理與應用程式. 因為只是要測試版面規劃, 所以標題, 歡迎詞, 日期, 主題布景等都是固定的.
另外中間版面的內容指定為 /systabs_1, 此路徑的處理類別如下 :
class systabs_1(webapp2.RequestHandler):
def get(self):
#check login session
url="templates/systabs_1.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
很簡單就是渲染網頁模板 systabs_1.htm 如下, 是一個 EasyUI 頁籤面板元件, 包含兩個頁籤, 來源分別是 /home 與 /admin 路徑 :
<div id="sys_tabs" class="easyui-tabs" data-options="fit:'true'">
<div class="tab" title="首頁" data-options="href:'/home',loadingMessage:'載入中 ... '"></div>
<div class="tab" title="管理" data-options="href:'/admin',loadingMessage:'載入中 ... '"></div>
</div>
而 /home 與 /admin 路徑也是簡單地渲染 home.htm 與 admin.htm 兩個網頁, home.htm 在上一篇文章中已用過, 就是輸出 Welcome! 而已. 路徑 /admin 的處理類別如下 :
class admin(webapp2.RequestHandler):
def get(self):
url="templates/admin.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
{% extends "jqueryeasyui.htm" %}
{% block style %}
body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
<p>Admin only!</p>
{% endblock%}
以上只是為整個 CMS 系統打個版面架構, 下面進行細部功能實現.
先處理主題佈景部分, 在版面右上方有一個用 combobox 做的主題佈景選擇器, 使用者可以隨時切換主題佈景, 而且每次切換會被記錄在資料庫裡, 作為下一次登入時的預設佈景. 這在 PHP 可以很簡單地利用 Session 來記住使用者目前的佈景設定, 參考 :
# 用 jQuery EasyUI 打造輕量級 CMS (五)
在 GAE 的 webapp2 框架也提供了 Sessions 模組可用來在伺服端記住使用者狀態 (Session 功能在早期 webapp 框架時代是不提供的, 但可以利用 Memcache API 來模擬, 參考上官林傑 "Google 應用服務引擎開發實戰" 10-2 節, P287). 參考 :
# Using SESSION with Google App Engine and Python 2.7
# Webapp2 Sessions in Google app engine
# How to handle sessions in Google app engine
# Webapp2 Sessions
參考上面四篇作法, 欲使用 sessions 模組, 需從 webapp2_extra 匯入模組 sessions, 我在 main.py 中加入這行 :
from webapp2_extras import sessions
然後新增一個 BaseHandler 類別 :
class BaseHandler(webapp2.RequestHandler):
def dispatch(self):
# Get a session store for this request.
self.session_store=sessions.get_store(request=self.request)
try:
#Dispatch the request.
webapp2.RequestHandler.dispatch(self)
finally:
#Save all sessions.
self.session_store.save_sessions(self.response)
@webapp2.cached_property
def session(self):
#Returns a session using the default cookie key.
sess=self.session_store.get_session()
#add some default values:
if not sess.get("theme"):
sess["theme"]="default"
return sess
這裡我們呼叫 get_session() 方法取得 Session 實體集合, 然後回傳給呼叫 session() 方法的使用者. 不過在回傳前設定特定 Session 物件的預設值, 例如我想讓使用者透過下拉式選單隨時更換網站的主題佈景, 由於 HTTP 是無狀態協定, 如何讓網站伺服器記得我們目前的佈景設定呢? 這就得依賴伺服端的Session 連線物件來記錄了. 這裡利用 Session 物件實體的 get() 方法來判斷伺服器是否已有名為 theme 的 Session, 沒有的話就產生一個, 且其值設為 EasyUI 的預設佈景 "default". 任何需要設定初始值的連線物件都可以在這裡用 if 來設值.
除此之外, 使用 sessions 模組還必須設定 secret key, 所以我依照 Webapp2 Sessions 這篇所述, 在 main.py 中新增一個 config 物件, 設定 webapp2_extra.sessions 屬性值為一個含有 secret_key 屬性之物件 (其值可任意定, 我只是照抄), 然後將此 config 物件傳給 webapp2.WSGIApplication() 方法的 config 參數即可, 如下所示 :
config={}
config['webapp2_extras.sessions']={'secret_key':'my-super-secret-key'}
app = webapp2.WSGIApplication([
('/', MainHandler),
('/easyui_1', easyui_1),
('/easyui_2', easyui_2),
('/easyui_3', easyui_3),
('/easyui_4', easyui_4),
('/login_4', login_4),
........
('/get_visitors_6', get_visitors_6),
('/main_1', main_1),
('/login', login),
('/login_check', login_check)
], debug=True, config=config)
注意, 所有需要用到 Session 功能的類別都要繼承這個 BaseHandler 類別來處理連線狀態, 事實上也已經繼承了 webapp2.RequestHandler 類別, 也可以處理來自前端的要求.
搞定 Session 功能後就可以進行登入功能的改寫, 當使用者登入系統經過資料庫驗證為合法使用者之後, 就在伺服端產生 Session 物件來記錄登入狀態, 每個系統功能的 url 處理器在開頭的地方都檢驗是否存在連線物件, 有的話即許可進入, 否則全部導向登入畫面, 這是網站安全控管的必要措施.
但在此之前還須做一下資料庫配置. 首先, 為了方便產生主題佈景選單 (即讓被選的 selected), 我在 model.py 中新增 Themes 這個 Model 來記錄 EasyUI 內建的幾種佈景 :
class Themes(db.Model):
theme=db.StringProperty()
theme=Themes(key_name="default",theme="default")
theme.put()
theme=Themes(key_name="gray",theme="gray")
theme.put()
theme=Themes(key_name="black",theme="black")
theme.put()
theme=Themes(key_name="bootstrap",theme="bootstrap")
theme.put()
theme=Themes(key_name="metro",theme="metro")
theme.put()
theme=Themes(key_name="metro-blue",theme="metro-blue")
theme.put()
theme=Themes(key_name="metro-gray",theme="metro-gray")
theme.put()
theme=Themes(key_name="metro-green",theme="metro-green")
theme.put()
theme=Themes(key_name="metro-orange",theme="metro-orange")
theme.put()
theme=Themes(key_name="metro-red",theme="metro-red")
theme.put()
theme=Themes(key_name="ui-cupertino",theme="ui-cupertino")
theme.put()
theme=Themes(key_name="ui-dark-hive",theme="ui-dark-hive")
theme.put()
theme=Themes(key_name="ui-pepper-grinder",theme="ui-pepper-grinder")
theme.put()
theme=Themes(key_name="ui-sunny",theme="ui-sunny")
theme.put()
注意, 這裡每一個資料實體 (entity, 即紀錄也) 都要設一個 key_name, 方便擷取紀錄時可以用 Model 的 get_by_key_name() 方法來直接取出資料, 雖然在 Themes 這個資料表用不到, 但設 key_name 才不會在每次載入 model.py 時重複產生資料實體, 導致同樣的實體一大堆.
其次, 為了記錄使用者個人的主題佈景, 以便下次登入時能顯示上一次登出前的佈景, 我修改了使用者資料表 Members, 增加 theme 與 is_admin 這兩個欄位, 每次當使用者更改其佈景時, 也會同時更新此欄位, 而 is_admin 則是用來標示使用者是否為管理員, 主要是控制可使用的功能, 例如 Tab 或按鈕等等 :
class Members(db.Model):
account=db.StringProperty()
password=db.StringProperty()
theme=db.StringProperty()
is_admin=db.BooleanProperty()
這裡的 theme 欄位用來記錄個人的主題佈景, 登入成功後會將此值寫入名為 theme 的 Session 中, 以便在應用程式中套用. 而 is_admin 欄位用來標示此用戶是否為管理員, 此值也會寫入名為 is_admin 的 Session 中, 用來控制應用程式中只能讓管理員看的部分. 同樣地, 我們也要設 key_name, 此處以具有唯一性的 account 欄位值作為 key_name 之值.
另外, 我也新增了一個 Settings 資料表來記錄網站名稱, 網站預設主題佈景 (用在登入頁面), 網站狀態 (運轉中/維護中) 等 :
class Settings(db.Model):
site_title=db.StringProperty()
site_theme=db.StringProperty()
site_state=db.StringProperty()
site_created_time=db.DateTimeProperty(auto_now_add=True)
settings=Settings(key_name="settings",site_title="EasyUI-based CMS on GAE",
site_theme="ui-sunny",site_state="")
settings.put()
其中, site_theme 用在登入對話框的, 讓管理者可以隨時更改預設門面, 而非寫死在程式裡. 但使用者登入後就會改成使用者個人佈景了. 此資料表只會有一個實體, 為了存取方便也為其設了一個名為 "setings" 的 key name.
經過這樣安排就可以開始寫登入頁面了, 此 login 路徑處理類別如下 :
class login(webapp2.RequestHandler):
def get(self):
s=m.Settings.get_by_key_name("settings")
info={}
info["site_title"]=s.site_title
info["site_theme"]=s.site_theme
url="templates/login.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{"info":info})
self.response.out.write(content)
這裡首先用 get_by_key_name() 方法來取出名為 settings 的資料實體, 將其中的 site_title 與 site_theme 欄位存入名為 info 的字典物件中, 然後在渲染登入頁面模板 login.htm 時將 info 物件傳給它. 此 login.htm 登入頁面是從上一篇的測試 3 所使用的 easyui_5_1 類別複製過來修改的, 只是將 easyui_5_1 改為 login 而已, 當然所渲染的 login.htm 也是從 easyui_5_1.htm 複製過來修改為 login.htm 如下 :
{% extends "jqueryeasyui.htm" %}
{% block body %}
<div id="login-dialog" class="easyui-dialog" title="{{info.site_title}} 系統登入" style="width:370px;height:230px;padding:10px" buttons="#login-buttons">
<div style="margin:5px;border-bottom:1px solid #ccc;">
<p id="msg">請輸入帳號密碼</p>
</div>
<form id="login-form" method="post" style="padding:10px 30px;">
<div style="margin:5px">
<label style="width:60px;display:inline-block;">帳號 : </label>
<input id="account" name="account" type="text" class="easyui-textbox" required="true" data-options="iconCls:'icon-man',missingMessage:'此欄位為必填'" style="width:200px">
</div>
<div style="margin:5px">
<label style="width:60px;display:inline-block;">密碼 : </label>
<input name="password" type="password" class="easyui-textbox" required="true" data-options="iconCls:'icon-lock',missingMessage:'此欄位為必填'" style="width:200px">
</div>
</form>
</div>
<div id="login-buttons" style="padding-right:15px;">
<a href="#" id="cancel" class="easyui-linkbutton" iconCls="icon-cancel" style="width:90px">取消</a>
<a href="#" id="login" class="easyui-linkbutton" iconCls="icon-ok" style="width:90px">登入</a>
</div>
<script type="text/javascript">
$(document).ready(function(){
var css="/static/easyui/themes/{{info.site_theme}}/easyui.css";
$("#theme").attr("href", css); //更改主題佈景
$("#login-form").form({ //設定表單
url:"/login_check",
success:function(data){
var data=eval('(' + data + ')'); //將 JSON 轉成物件
if (data.result=="success") {window.location.href='/main_2';}
else {$("#msg").text("帳號或密碼錯誤!");}
}
});
$("#account").textbox('clear').textbox('textbox').focus();
$("#login").bind("click",function(){
$("#login-form").submit(); //提交表單進行驗證
});
$("#cancel").bind("click",function(){
$("#login-form").form("reset"); //清除表單欄位
});
});
</script>
{% endblock %}
這裡我們利用從 main.py 傳進來的 info 物件來設定登入對話框的標題, 以及透過 jQuery 的 attr() 方法修改頁面只修改了兩個跟 URL 有關的地方, 一是按下確定鈕後的驗證程式改為 login_check, 二是驗證通過後要導向的目的地路徑設為 main_2, 此路徑處理程式將渲染一個網頁模板 main_2.htm, 由上面測試 1 打好的網頁版面 main_1.htm 修改而來.
注意, 這裡我意外找到之前 EasyUI Dialog box 無法聚焦於其中的輸入框問題, 原來是要用 textbox() 方法取得輸入框元件後再呼叫 focus() 方法 :
$("#account").textbox('clear').textbox('textbox').focus();
參考 :
# how can i focus the textbox
首先處理登入檢查程式 login_check, 也是紀錄連線狀態的關鍵程式, 我們在 main.py 中新增 login_check 路徑與其處理類別如下 :
class login_check(BaseHandler):
def post(self):
account=self.request.get("account", default_value="unknown")
password=self.request.get("password", default_value="unknown")
query=m.Members.gql("""WHERE account= :1
AND password= :2""",
account,password)
result=query.get()
if result is None:
content='{"result":"failure","reason":"帳號或密碼錯誤"}'
else:
self.session['account']=result.account #save session
self.session['theme']=result.theme #save session
self.session['is_admin']=result.is_admin #save session
content='{"result":"success"}'
self.response.out.write(content)
事實上這個 login_check 是從上一篇的 login_5 類別複製過來修改的, 只是在 else 區塊中加入三個 session 變數的設定而已, 就是將使用者資料實體的 account, theme, 以及 is_admin 三個欄位值存到 session 中, 以利後續應用程式使用.
在 login.htm 中, 登入成功後 jQuery 會將頁面導向 /main_2 路徑, 其處理類別如下 :
class main_2(BaseHandler):
def get(self):
info={} #for storing parameters
#check login session
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
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()]
theme_list=[]
themes=m.Themes.all()
#themes.order("theme")
for t in themes:
theme_list.append(t.theme)
info["themes"]=theme_list
url="templates/main_2.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)
# update an entity in datastore app engine
# Multiple datastore entities with the same ID!
# Google App Engine 初學(7) – Db Data Store 操作 (GQL)
# Khai's personal knowledge vault
這次測試也搜尋到幾個不錯的 Python 的學習資料如下 :
# Learn Python the hard way
# Python Module of the Week
# Eric Park 的 Python 筆記
首先我參考了之前使用 PHP 版的文章先做好 GAE 版的版面配置, 記得當時可是花了一整天才調好自訂的樣式表, 得到了一個滿意的版面, 參考下面這篇 PHP 版的 :
# 用 jQuery EasyUI 打造輕量級 CMS (一)
在 PHP 版的 CMS 裡, 此網頁稱為 main.php, 而在 GAE 是利用 Python 程式的 main.py 去渲染網頁模板, 所以我把 CMS 的網頁模板取名為 main_1.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="EasyUI-based CMS on GAE" 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">
您好! admin, 今天是 2016 年 01 月 12 日 星期二
</td>
<td id="north-right" style="vertical-align:middle">
<span id="header_links">
<a href="javascript:gohome()" target="_self" title="首頁">首頁</a>.
<a href="javascript:logout()" target="_self" title="登出">登出</a>.
</span>
<select id="theme_sel" name="theme" class="easyui-combobox" style="width:120px;height:18px" panelHeight="auto">
<option value="default">主題布景</option>
<option value="default" selected>default</option>
<option value="gray">gray</option>
<option value="black">black</option>
<option value="bootstrap">bootstrap</option>
<option value="metro">metro</option>
<option value="metro-blue">metro-blue</option>
<option value="metro-gray">metro-gray</option>
<option value="metro-green">metro-green</option>
<option value="metro-orange">metro-orange</option>
<option value="metro-red">metro-red</option>
<option value="ui-cupertino">ui-cupertino</option>
<option value="ui-dark-hive">ui-dark-hive</option>
<option value="ui-pepper-grinder">ui-pepper-grinder</option>
<option value="ui-sunny">ui-sunny</option>
</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_1'" id="center">
</div>
<script>
//$(document.body).addClass("easyui-layout");
$("body").attr("class", "easyui-layout");
<!-- 系統的載入與登出函式 -->
function gohome(){
$(function(){
var p=$("body").layout("panel","center");
p.panel({href:"/systabs_1"});
});
}
function logout(){
$(function(){
$.messager.confirm("確認","確定要登出系統嗎?",function(btn){
if (btn) {window.location.href="/logout";}
});
});
}
</script>
{% endblock%}
測試 1 : http://jqueryeasyui.appspot.com/main_1 (下載原始碼) (備用下載點)
在這個網頁模板中, 我們使用 jQuery 的 attr() 方法將 body 元素加上 "easyui-layout" 樣式, 這樣便能利用 div 元素來進行排版. 版面分割為 north, left, 以及 center 三個版面, 上方的 north 用來放置網站標題, 日期與問候語, 應用程式超連結, 以及主題切換器. 左方是導覽列, 中央則是內容區, 用來呈現系統設管理與應用程式. 因為只是要測試版面規劃, 所以標題, 歡迎詞, 日期, 主題布景等都是固定的.
另外中間版面的內容指定為 /systabs_1, 此路徑的處理類別如下 :
class systabs_1(webapp2.RequestHandler):
def get(self):
#check login session
url="templates/systabs_1.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
很簡單就是渲染網頁模板 systabs_1.htm 如下, 是一個 EasyUI 頁籤面板元件, 包含兩個頁籤, 來源分別是 /home 與 /admin 路徑 :
<div id="sys_tabs" class="easyui-tabs" data-options="fit:'true'">
<div class="tab" title="首頁" data-options="href:'/home',loadingMessage:'載入中 ... '"></div>
<div class="tab" title="管理" data-options="href:'/admin',loadingMessage:'載入中 ... '"></div>
</div>
而 /home 與 /admin 路徑也是簡單地渲染 home.htm 與 admin.htm 兩個網頁, home.htm 在上一篇文章中已用過, 就是輸出 Welcome! 而已. 路徑 /admin 的處理類別如下 :
class admin(webapp2.RequestHandler):
def get(self):
url="templates/admin.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
而 admin.htm 則複製 home.htm, 只改變輸出為 Admin only! 而已 :
{% extends "jqueryeasyui.htm" %}
{% block style %}
body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
<p>Admin only!</p>
{% endblock%}
以上只是為整個 CMS 系統打個版面架構, 下面進行細部功能實現.
先處理主題佈景部分, 在版面右上方有一個用 combobox 做的主題佈景選擇器, 使用者可以隨時切換主題佈景, 而且每次切換會被記錄在資料庫裡, 作為下一次登入時的預設佈景. 這在 PHP 可以很簡單地利用 Session 來記住使用者目前的佈景設定, 參考 :
# 用 jQuery EasyUI 打造輕量級 CMS (五)
在 GAE 的 webapp2 框架也提供了 Sessions 模組可用來在伺服端記住使用者狀態 (Session 功能在早期 webapp 框架時代是不提供的, 但可以利用 Memcache API 來模擬, 參考上官林傑 "Google 應用服務引擎開發實戰" 10-2 節, P287). 參考 :
# Using SESSION with Google App Engine and Python 2.7
# Webapp2 Sessions in Google app engine
# How to handle sessions in Google app engine
# Webapp2 Sessions
參考上面四篇作法, 欲使用 sessions 模組, 需從 webapp2_extra 匯入模組 sessions, 我在 main.py 中加入這行 :
from webapp2_extras import sessions
然後新增一個 BaseHandler 類別 :
class BaseHandler(webapp2.RequestHandler):
def dispatch(self):
# Get a session store for this request.
self.session_store=sessions.get_store(request=self.request)
try:
#Dispatch the request.
webapp2.RequestHandler.dispatch(self)
finally:
#Save all sessions.
self.session_store.save_sessions(self.response)
@webapp2.cached_property
def session(self):
#Returns a session using the default cookie key.
sess=self.session_store.get_session()
#add some default values:
if not sess.get("theme"):
sess["theme"]="default"
return sess
這裡我們呼叫 get_session() 方法取得 Session 實體集合, 然後回傳給呼叫 session() 方法的使用者. 不過在回傳前設定特定 Session 物件的預設值, 例如我想讓使用者透過下拉式選單隨時更換網站的主題佈景, 由於 HTTP 是無狀態協定, 如何讓網站伺服器記得我們目前的佈景設定呢? 這就得依賴伺服端的Session 連線物件來記錄了. 這裡利用 Session 物件實體的 get() 方法來判斷伺服器是否已有名為 theme 的 Session, 沒有的話就產生一個, 且其值設為 EasyUI 的預設佈景 "default". 任何需要設定初始值的連線物件都可以在這裡用 if 來設值.
除此之外, 使用 sessions 模組還必須設定 secret key, 所以我依照 Webapp2 Sessions 這篇所述, 在 main.py 中新增一個 config 物件, 設定 webapp2_extra.sessions 屬性值為一個含有 secret_key 屬性之物件 (其值可任意定, 我只是照抄), 然後將此 config 物件傳給 webapp2.WSGIApplication() 方法的 config 參數即可, 如下所示 :
config={}
config['webapp2_extras.sessions']={'secret_key':'my-super-secret-key'}
app = webapp2.WSGIApplication([
('/', MainHandler),
('/easyui_1', easyui_1),
('/easyui_2', easyui_2),
('/easyui_3', easyui_3),
('/easyui_4', easyui_4),
('/login_4', login_4),
........
('/get_visitors_6', get_visitors_6),
('/main_1', main_1),
('/login', login),
('/login_check', login_check)
], debug=True, config=config)
如果沒有設定 secret_key 的話, 會出現如下錯誤訊息 :
Exception: Missing configuration keys for 'webapp2_extras.sessions': ['secret_key'].
搞定 Session 功能後就可以進行登入功能的改寫, 當使用者登入系統經過資料庫驗證為合法使用者之後, 就在伺服端產生 Session 物件來記錄登入狀態, 每個系統功能的 url 處理器在開頭的地方都檢驗是否存在連線物件, 有的話即許可進入, 否則全部導向登入畫面, 這是網站安全控管的必要措施.
但在此之前還須做一下資料庫配置. 首先, 為了方便產生主題佈景選單 (即讓被選的 selected), 我在 model.py 中新增 Themes 這個 Model 來記錄 EasyUI 內建的幾種佈景 :
class Themes(db.Model):
theme=db.StringProperty()
theme=Themes(key_name="default",theme="default")
theme.put()
theme=Themes(key_name="gray",theme="gray")
theme.put()
theme=Themes(key_name="black",theme="black")
theme.put()
theme=Themes(key_name="bootstrap",theme="bootstrap")
theme.put()
theme=Themes(key_name="metro",theme="metro")
theme.put()
theme=Themes(key_name="metro-blue",theme="metro-blue")
theme.put()
theme=Themes(key_name="metro-gray",theme="metro-gray")
theme.put()
theme=Themes(key_name="metro-green",theme="metro-green")
theme.put()
theme=Themes(key_name="metro-orange",theme="metro-orange")
theme.put()
theme=Themes(key_name="metro-red",theme="metro-red")
theme.put()
theme=Themes(key_name="ui-cupertino",theme="ui-cupertino")
theme.put()
theme=Themes(key_name="ui-dark-hive",theme="ui-dark-hive")
theme.put()
theme=Themes(key_name="ui-pepper-grinder",theme="ui-pepper-grinder")
theme.put()
theme=Themes(key_name="ui-sunny",theme="ui-sunny")
theme.put()
注意, 這裡每一個資料實體 (entity, 即紀錄也) 都要設一個 key_name, 方便擷取紀錄時可以用 Model 的 get_by_key_name() 方法來直接取出資料, 雖然在 Themes 這個資料表用不到, 但設 key_name 才不會在每次載入 model.py 時重複產生資料實體, 導致同樣的實體一大堆.
其次, 為了記錄使用者個人的主題佈景, 以便下次登入時能顯示上一次登出前的佈景, 我修改了使用者資料表 Members, 增加 theme 與 is_admin 這兩個欄位, 每次當使用者更改其佈景時, 也會同時更新此欄位, 而 is_admin 則是用來標示使用者是否為管理員, 主要是控制可使用的功能, 例如 Tab 或按鈕等等 :
class Members(db.Model):
account=db.StringProperty()
password=db.StringProperty()
theme=db.StringProperty()
is_admin=db.BooleanProperty()
member=Members(key_name="admin",account="admin",password="aaa",
theme="default",is_admin=True)
member.put()
member=Members(key_name="guest",account="guest",password="guest",
theme="black",is_admin=False)
member.put()
這裡的 theme 欄位用來記錄個人的主題佈景, 登入成功後會將此值寫入名為 theme 的 Session 中, 以便在應用程式中套用. 而 is_admin 欄位用來標示此用戶是否為管理員, 此值也會寫入名為 is_admin 的 Session 中, 用來控制應用程式中只能讓管理員看的部分. 同樣地, 我們也要設 key_name, 此處以具有唯一性的 account 欄位值作為 key_name 之值.
另外, 我也新增了一個 Settings 資料表來記錄網站名稱, 網站預設主題佈景 (用在登入頁面), 網站狀態 (運轉中/維護中) 等 :
class Settings(db.Model):
site_title=db.StringProperty()
site_theme=db.StringProperty()
site_state=db.StringProperty()
site_created_time=db.DateTimeProperty(auto_now_add=True)
settings=Settings(key_name="settings",site_title="EasyUI-based CMS on GAE",
site_theme="ui-sunny",site_state="")
settings.put()
其中, site_theme 用在登入對話框的, 讓管理者可以隨時更改預設門面, 而非寫死在程式裡. 但使用者登入後就會改成使用者個人佈景了. 此資料表只會有一個實體, 為了存取方便也為其設了一個名為 "setings" 的 key name.
經過這樣安排就可以開始寫登入頁面了, 此 login 路徑處理類別如下 :
class login(webapp2.RequestHandler):
def get(self):
s=m.Settings.get_by_key_name("settings")
info={}
info["site_title"]=s.site_title
info["site_theme"]=s.site_theme
url="templates/login.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{"info":info})
self.response.out.write(content)
這裡首先用 get_by_key_name() 方法來取出名為 settings 的資料實體, 將其中的 site_title 與 site_theme 欄位存入名為 info 的字典物件中, 然後在渲染登入頁面模板 login.htm 時將 info 物件傳給它. 此 login.htm 登入頁面是從上一篇的測試 3 所使用的 easyui_5_1 類別複製過來修改的, 只是將 easyui_5_1 改為 login 而已, 當然所渲染的 login.htm 也是從 easyui_5_1.htm 複製過來修改為 login.htm 如下 :
{% extends "jqueryeasyui.htm" %}
{% block body %}
<div id="login-dialog" class="easyui-dialog" title="{{info.site_title}} 系統登入" style="width:370px;height:230px;padding:10px" buttons="#login-buttons">
<div style="margin:5px;border-bottom:1px solid #ccc;">
<p id="msg">請輸入帳號密碼</p>
</div>
<form id="login-form" method="post" style="padding:10px 30px;">
<div style="margin:5px">
<label style="width:60px;display:inline-block;">帳號 : </label>
<input id="account" name="account" type="text" class="easyui-textbox" required="true" data-options="iconCls:'icon-man',missingMessage:'此欄位為必填'" style="width:200px">
</div>
<div style="margin:5px">
<label style="width:60px;display:inline-block;">密碼 : </label>
<input name="password" type="password" class="easyui-textbox" required="true" data-options="iconCls:'icon-lock',missingMessage:'此欄位為必填'" style="width:200px">
</div>
</form>
</div>
<div id="login-buttons" style="padding-right:15px;">
<a href="#" id="cancel" class="easyui-linkbutton" iconCls="icon-cancel" style="width:90px">取消</a>
<a href="#" id="login" class="easyui-linkbutton" iconCls="icon-ok" style="width:90px">登入</a>
</div>
<script type="text/javascript">
$(document).ready(function(){
var css="/static/easyui/themes/{{info.site_theme}}/easyui.css";
$("#theme").attr("href", css); //更改主題佈景
$("#login-form").form({ //設定表單
url:"/login_check",
success:function(data){
var data=eval('(' + data + ')'); //將 JSON 轉成物件
if (data.result=="success") {window.location.href='/main_2';}
else {$("#msg").text("帳號或密碼錯誤!");}
}
});
$("#account").textbox('clear').textbox('textbox').focus();
$("#login").bind("click",function(){
$("#login-form").submit(); //提交表單進行驗證
});
$("#cancel").bind("click",function(){
$("#login-form").form("reset"); //清除表單欄位
});
});
</script>
{% endblock %}
這裡我們利用從 main.py 傳進來的 info 物件來設定登入對話框的標題, 以及透過 jQuery 的 attr() 方法修改頁面只修改了兩個跟 URL 有關的地方, 一是按下確定鈕後的驗證程式改為 login_check, 二是驗證通過後要導向的目的地路徑設為 main_2, 此路徑處理程式將渲染一個網頁模板 main_2.htm, 由上面測試 1 打好的網頁版面 main_1.htm 修改而來.
注意, 這裡我意外找到之前 EasyUI Dialog box 無法聚焦於其中的輸入框問題, 原來是要用 textbox() 方法取得輸入框元件後再呼叫 focus() 方法 :
$("#account").textbox('clear').textbox('textbox').focus();
參考 :
# how can i focus the textbox
class login_check(BaseHandler):
def post(self):
account=self.request.get("account", default_value="unknown")
password=self.request.get("password", default_value="unknown")
query=m.Members.gql("""WHERE account= :1
AND password= :2""",
account,password)
result=query.get()
if result is None:
content='{"result":"failure","reason":"帳號或密碼錯誤"}'
else:
self.session['account']=result.account #save session
self.session['theme']=result.theme #save session
self.session['is_admin']=result.is_admin #save session
content='{"result":"success"}'
self.response.out.write(content)
事實上這個 login_check 是從上一篇的 login_5 類別複製過來修改的, 只是在 else 區塊中加入三個 session 變數的設定而已, 就是將使用者資料實體的 account, theme, 以及 is_admin 三個欄位值存到 session 中, 以利後續應用程式使用.
在 login.htm 中, 登入成功後 jQuery 會將頁面導向 /main_2 路徑, 其處理類別如下 :
class main_2(BaseHandler):
def get(self):
info={} #for storing parameters
#check login session
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
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()]
theme_list=[]
themes=m.Themes.all()
#themes.order("theme")
for t in themes:
theme_list.append(t.theme)
info["themes"]=theme_list
url="templates/main_2.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)
此類別會依據 session 物件變數 account 判別用戶是否已登入, 是的話就渲染 main_2.htm 網頁模板, 否則渲染 login.htm 強制導向登入頁面. 若用戶已登入, 就將 account 存入 info 字典, 然後從 session 變數 theme 取得主題佈景存入 info 字典索引 theme 裡. 接著呼叫 datetime.date.today() 取得今日物件以製作歡迎詞, 存入字典索引 greeting 中. 最後是讀取 Themes 資料表, 利用串列儲存主題佈景名稱 (呼叫串列的 append 方法), 存入字典索引 themes 中. 如果用戶未登入, 就從資料表 settings 中取出 site_title 與 site_theme 傳給登入頁面.
這裡取得星期幾是利用 today 物件的 weekday() 方法, 它會傳回 0~6, 0 是星期一, 2~6 是星期二到星期日, 所以 week 串列要這樣安排 :
week=[u"一",u"二",u"三",u"四",u"五",u"六",u"日"]
參考 :
# pyMOTW : datetime – Date/time value manipulation
# python 获得日期是星期几
# python datetime extract double digit month and day values
注意, 為了能正確顯示中文, 所有中文字串我都採用在前面加 u 來標示使用 unicode 的 utf-8 編碼, 同時所有檔案, 不論是 python 或 html 模板檔案, 都是以 utf-8 編碼存檔, 關於 GAE 上的中文問題, 參考下面這篇 :
# 解決方法: UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
網頁模板 main_2.htm 是從上面的 main_1.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">
<a href="javascript:gohome()" target="_self" title="首頁">首頁</a>.
<a href="javascript:logout()" target="_self" title="登出">登出</a>.
</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_2"});
});
}
function logout(){
$(function(){
$.messager.confirm("確認","確定要登出系統嗎?",function(btn){
if (btn) {window.location.href="/logout";}
});
});
}
</script>
{% endblock%}
這裡傳進來的 info.site_title 輸出在版面北方 div 的 title 中, 而 info.themes 則用來以 for 迴圈產生 combobox 元件的 option 選項, 當其值與使用者的個人主題佈景相同時會加上 selected 屬性, 讓下拉式選單顯示此選項為被選擇狀態. 至於 info.theme 則用來將從 jqueryeasyui.htm 繼承過來的預設主題佈景 default 改為使用者自己的佈景.
當使用者用下拉式選單更改佈景時, 會先用 jQuery 的 attr() 方法更改 Link 元素中的 easyui.css 檔的路徑, 這會立即改變頁面外觀, 其次會以 Ajax 的 get 方法向後端 /change_theme 提出要求, 並傳出 theme 參數, 因為 Combobox 元件裡的 select 元素的 name 為 theme 之故. 其路徑處理類別如下 :
class change_theme(BaseHandler):
def get(self):
theme=self.request.get("theme") #get selected theme
self.session['theme']=theme #update user session
account=self.session.get('account')
member=m.Members.get_by_key_name(account) #retrieve entity
member.theme=theme
member.put() #update user theme in datastre
這裡因為要用到 session 物件, 因此要繼承 BaseHandler, 利用 self.requst.get() 方法取得前端主題佈景選擇器傳來的 theme 參數後, 先更新 session 變數 theme, 再到資料儲存的 Members 資料表取得此使用者之資料實體, 更改其 theme 欄位為前端傳來之值即可.
接下來處理 main_2.htm 的中間面板部分. 在上面測試 1 中, 中間面板只是渲染一個兩個固定頁籤網頁, 但這裡要進一步實現動態頁籤輸出功能, 意即透過資料儲存來管理頁籤. 我在 model.py 中新增 Systabs 資料表如下, 同時建立兩個頁籤實體 :
class Systabs(db.Model):
tab_name=db.StringProperty()
tab_label=db.StringProperty()
tab_link=db.StringProperty()
tab_tip=db.StringProperty()
tab_order=db.IntegerProperty()
tab_admin=db.BooleanProperty()
systab=Systabs(key_name="home",tab_name="home",tab_label=u"首頁",
tab_link="/home",tab_order=0,tab_admin=False)
systab.put()
systab=Systabs(key_name="admin",tab_name="admin",tab_label=u"管理",
tab_link="/admin",tab_order=1,tab_admin=True)
systab.put()
此處的重要欄位是要顯示在頁籤上的 tab_label, 其處理路徑 tab_link, 頁籤顯示順序的 tab_order, 以及是否為管理員使用頁籤的 tab_admin, 而 tab_name 欄位我現在也不太確定需不需要, 我只是先照搬 PHP 版的過來而已.
路徑 /systabs_2 的處理類別如下 :
class systabs_2(BaseHandler):
def get(self):
systabs=m.Systabs.all()
systabs.order("tab_order") #sort by tab_order
tabs=[] #for storing tab objects
is_admin=self.session.get('is_admin') #True/False
for t in systabs:
tab={}
tab["tab_label"]=t.tab_label
tab["tab_link"]=t.tab_link
if t.tab_admin: #this tab is for admin only
if is_admin: #current user is admin
tabs.append(tab)
else: #this tab is for registered users
tabs.append(tab)
url="templates/systabs_2.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{"tabs":tabs})
self.response.out.write(content)
這裡是先讀取 Systabs 資料表中全部實體, 以 tab_order 欄位排序後利用迴圈拜訪 Query 物件, 然後把每一個頁籤實體的 tab_label 與 tab_link 存入一個 tab 字典物件, 如果是管理者頁籤且使用者是管理者, 就把 tab 物件用 append() 方法存入 tabs 串列, 如果不是管理者頁籤, 都一律會存入 tabs 串列, 最後把 tabs 串列作為變數在渲染 systabs_2.htm 網頁時傳給它. 此 systabs_2.htm 是由 systabs_1.htm 複製修改而來, 內容如下 :
<div id="sys_tabs" class="easyui-tabs" data-options="fit:'true'">
{% for t in tabs %}
<div class="tab" title="{{t.tab_label}}" data-options="href:'{{t.tab_link}}',loadingMessage:'載入中 ... '"></div>
{% endfor %}
</div>
這裡用 for 迴圈來拜訪所傳入的 tabs 變數, 然後將 tab_label 填入 div 元素的title 屬性, 將 tab_link 填入 href 屬性中即可.
這個 main_2.htm 中還有一個登出功能尚未實現, 當按登出連結或按鈕時會向後端 /logout 路徑提出要求, 其路徑處理類別如下 :
class logout(BaseHandler):
def get(self):
#check login session
self.session.clear()
self.redirect("/login")
它會先呼叫 session 物件的 clear() 方法將此連線相關的 session 變數全部清除, 然後將頁面重導向至 /login 路徑, 即登入頁面.
以上就是這次測試的全紀錄, 實際測試網頁及 zip 原始碼在此 (裡面包含了自備的 Easyui 1.4 版) :
測試 2 : http://jqueryeasyui.appspot.com/main_2 (下載原始碼) (備用下載點)
如果是以非管理者的 guest 帳號登入, 則沒有 "管理" 頁籤, 可見 Members 裡的 is_admin 欄位達成權限管控目的 :
自從 2010 年開始接觸到 GAE 以後, 我就想能否在 GAE 上面寫個 CMS 呢? 現在終於實現了! 從 1/12 開始動手, 中間被其他事務打斷, 斷斷續續來回測試到 OK 超過半個月. 雖然目前還只是陽春的架構, 但至少透過實作證實了自己的猜想, 還有其他像檔案上傳, 留言板, 網站管理, 加掛應用程式等 CMS 主要功能待實現, 以後再一步步加上去.
這次的測試也發現了一些新作法, 例如 site_theme 欄位就是 PHP 版所沒有的, 其次是主題佈景下拉式選單的高度 pannelHeight 可以設為 'auto', 這樣底下就不會留白了, 這些在下次修改 PHP 版本時可以考慮進去. 我不一定要把 PHP 版移植到 GAE 上, 畢竟我最熟悉的還是 PHP, 我的 Python 程度還在打游擊階段, 邊學邊賣, 困而習之而已, 我知道 Python 很強, 需要有時間去系統化的學習才能掌握, 學 Python 不光是為了 GAE 而已, Raspberry Pi 也用得到.
參考資料 :這裡取得星期幾是利用 today 物件的 weekday() 方法, 它會傳回 0~6, 0 是星期一, 2~6 是星期二到星期日, 所以 week 串列要這樣安排 :
week=[u"一",u"二",u"三",u"四",u"五",u"六",u"日"]
參考 :
# pyMOTW : datetime – Date/time value manipulation
# python 获得日期是星期几
# python datetime extract double digit month and day values
注意, 為了能正確顯示中文, 所有中文字串我都採用在前面加 u 來標示使用 unicode 的 utf-8 編碼, 同時所有檔案, 不論是 python 或 html 模板檔案, 都是以 utf-8 編碼存檔, 關於 GAE 上的中文問題, 參考下面這篇 :
# 解決方法: UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
網頁模板 main_2.htm 是從上面的 main_1.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">
<a href="javascript:gohome()" target="_self" title="首頁">首頁</a>.
<a href="javascript:logout()" target="_self" title="登出">登出</a>.
</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_2"});
});
}
function logout(){
$(function(){
$.messager.confirm("確認","確定要登出系統嗎?",function(btn){
if (btn) {window.location.href="/logout";}
});
});
}
</script>
{% endblock%}
這裡傳進來的 info.site_title 輸出在版面北方 div 的 title 中, 而 info.themes 則用來以 for 迴圈產生 combobox 元件的 option 選項, 當其值與使用者的個人主題佈景相同時會加上 selected 屬性, 讓下拉式選單顯示此選項為被選擇狀態. 至於 info.theme 則用來將從 jqueryeasyui.htm 繼承過來的預設主題佈景 default 改為使用者自己的佈景.
當使用者用下拉式選單更改佈景時, 會先用 jQuery 的 attr() 方法更改 Link 元素中的 easyui.css 檔的路徑, 這會立即改變頁面外觀, 其次會以 Ajax 的 get 方法向後端 /change_theme 提出要求, 並傳出 theme 參數, 因為 Combobox 元件裡的 select 元素的 name 為 theme 之故. 其路徑處理類別如下 :
class change_theme(BaseHandler):
def get(self):
theme=self.request.get("theme") #get selected theme
self.session['theme']=theme #update user session
account=self.session.get('account')
member=m.Members.get_by_key_name(account) #retrieve entity
member.theme=theme
member.put() #update user theme in datastre
這裡因為要用到 session 物件, 因此要繼承 BaseHandler, 利用 self.requst.get() 方法取得前端主題佈景選擇器傳來的 theme 參數後, 先更新 session 變數 theme, 再到資料儲存的 Members 資料表取得此使用者之資料實體, 更改其 theme 欄位為前端傳來之值即可.
接下來處理 main_2.htm 的中間面板部分. 在上面測試 1 中, 中間面板只是渲染一個兩個固定頁籤網頁, 但這裡要進一步實現動態頁籤輸出功能, 意即透過資料儲存來管理頁籤. 我在 model.py 中新增 Systabs 資料表如下, 同時建立兩個頁籤實體 :
class Systabs(db.Model):
tab_name=db.StringProperty()
tab_label=db.StringProperty()
tab_link=db.StringProperty()
tab_tip=db.StringProperty()
tab_order=db.IntegerProperty()
tab_admin=db.BooleanProperty()
systab=Systabs(key_name="home",tab_name="home",tab_label=u"首頁",
tab_link="/home",tab_order=0,tab_admin=False)
systab.put()
systab=Systabs(key_name="admin",tab_name="admin",tab_label=u"管理",
tab_link="/admin",tab_order=1,tab_admin=True)
systab.put()
此處的重要欄位是要顯示在頁籤上的 tab_label, 其處理路徑 tab_link, 頁籤顯示順序的 tab_order, 以及是否為管理員使用頁籤的 tab_admin, 而 tab_name 欄位我現在也不太確定需不需要, 我只是先照搬 PHP 版的過來而已.
路徑 /systabs_2 的處理類別如下 :
class systabs_2(BaseHandler):
def get(self):
systabs=m.Systabs.all()
systabs.order("tab_order") #sort by tab_order
tabs=[] #for storing tab objects
is_admin=self.session.get('is_admin') #True/False
for t in systabs:
tab={}
tab["tab_label"]=t.tab_label
tab["tab_link"]=t.tab_link
if t.tab_admin: #this tab is for admin only
if is_admin: #current user is admin
tabs.append(tab)
else: #this tab is for registered users
tabs.append(tab)
url="templates/systabs_2.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{"tabs":tabs})
self.response.out.write(content)
這裡是先讀取 Systabs 資料表中全部實體, 以 tab_order 欄位排序後利用迴圈拜訪 Query 物件, 然後把每一個頁籤實體的 tab_label 與 tab_link 存入一個 tab 字典物件, 如果是管理者頁籤且使用者是管理者, 就把 tab 物件用 append() 方法存入 tabs 串列, 如果不是管理者頁籤, 都一律會存入 tabs 串列, 最後把 tabs 串列作為變數在渲染 systabs_2.htm 網頁時傳給它. 此 systabs_2.htm 是由 systabs_1.htm 複製修改而來, 內容如下 :
<div id="sys_tabs" class="easyui-tabs" data-options="fit:'true'">
{% for t in tabs %}
<div class="tab" title="{{t.tab_label}}" data-options="href:'{{t.tab_link}}',loadingMessage:'載入中 ... '"></div>
{% endfor %}
</div>
這裡用 for 迴圈來拜訪所傳入的 tabs 變數, 然後將 tab_label 填入 div 元素的title 屬性, 將 tab_link 填入 href 屬性中即可.
這個 main_2.htm 中還有一個登出功能尚未實現, 當按登出連結或按鈕時會向後端 /logout 路徑提出要求, 其路徑處理類別如下 :
class logout(BaseHandler):
def get(self):
#check login session
self.session.clear()
self.redirect("/login")
它會先呼叫 session 物件的 clear() 方法將此連線相關的 session 變數全部清除, 然後將頁面重導向至 /login 路徑, 即登入頁面.
以上就是這次測試的全紀錄, 實際測試網頁及 zip 原始碼在此 (裡面包含了自備的 Easyui 1.4 版) :
測試 2 : http://jqueryeasyui.appspot.com/main_2 (下載原始碼) (備用下載點)
如果是以非管理者的 guest 帳號登入, 則沒有 "管理" 頁籤, 可見 Members 裡的 is_admin 欄位達成權限管控目的 :
自從 2010 年開始接觸到 GAE 以後, 我就想能否在 GAE 上面寫個 CMS 呢? 現在終於實現了! 從 1/12 開始動手, 中間被其他事務打斷, 斷斷續續來回測試到 OK 超過半個月. 雖然目前還只是陽春的架構, 但至少透過實作證實了自己的猜想, 還有其他像檔案上傳, 留言板, 網站管理, 加掛應用程式等 CMS 主要功能待實現, 以後再一步步加上去.
這次的測試也發現了一些新作法, 例如 site_theme 欄位就是 PHP 版所沒有的, 其次是主題佈景下拉式選單的高度 pannelHeight 可以設為 'auto', 這樣底下就不會留白了, 這些在下次修改 PHP 版本時可以考慮進去. 我不一定要把 PHP 版移植到 GAE 上, 畢竟我最熟悉的還是 PHP, 我的 Python 程度還在打游擊階段, 邊學邊賣, 困而習之而已, 我知道 Python 很強, 需要有時間去系統化的學習才能掌握, 學 Python 不光是為了 GAE 而已, Raspberry Pi 也用得到.
# update an entity in datastore app engine
# Multiple datastore entities with the same ID!
# Google App Engine 初學(7) – Db Data Store 操作 (GQL)
# Khai's personal knowledge vault
這次測試也搜尋到幾個不錯的 Python 的學習資料如下 :
# Learn Python the hard way
# Python Module of the Week
# Eric Park 的 Python 筆記
2016 第 5 周記事
忽然間 2016 年的第一個月就過去了, 今天已是 2/1, 堂堂邁入第六周矣. 過去的一周在忙甚麼? 有時候也很不容易回想, 有時候只是一些生活中的小事, 不值得掛懷, 但不記又似乎生活空白, 乏善可陳.
今年已正式步入五十歲, 標準的中年歐吉桑. 古人常有甚麼四十自述啦, 五十自述啦之類的回顧, 步入中年的人, 好像登上高山頂峰, 不禁會回首來時路, 也會眺望下山的風景究是如何. 人生到底是甚麼? 這是我最近常自問的一個問題. 老實說, 活到五十歲, 我也不知道人生的目的是甚麼, 或許要先問, 人生有目的嗎?
本周姊姊與菁菁同我回鄉下, 週日也一起上市集買菜, 由於前幾天寒流農損, 本周菜價急升, 連虱目魚肚都漲 15 元. 高橋邊的豆腐攤不知何故已兩周的周日沒來擺攤, 全聯買的板豆腐根本比不上. 今年菜園產的冬瓜剩下兩個, 過完年就會用完了, 我每周滷冬瓜的貨源就沒有啦! 要幫爸重新規劃一下三餐的準備.
上週五就離開家的大咪已超過一周未回, 雖然以前也好幾次出去很久才回家的紀錄, 但會擔心牠是否能挺過上周的低溫寒流呢? 所幸今日 2/1 打電話回去時, 爸說大咪終於回來了, 這隻老貓到底還是記得回家啊!
出租的農田週六打田, 週日就插秧了, 本想下周過年可以炕土窯的說. 本季因為前陣子冬雨不斷, 導致紅豆收成不佳, 雖然價格揚升, 但產量下降收入也沒比較多啊! 務農真的是靠天吃飯, 收入不穩定風險大.
今年已正式步入五十歲, 標準的中年歐吉桑. 古人常有甚麼四十自述啦, 五十自述啦之類的回顧, 步入中年的人, 好像登上高山頂峰, 不禁會回首來時路, 也會眺望下山的風景究是如何. 人生到底是甚麼? 這是我最近常自問的一個問題. 老實說, 活到五十歲, 我也不知道人生的目的是甚麼, 或許要先問, 人生有目的嗎?
本周姊姊與菁菁同我回鄉下, 週日也一起上市集買菜, 由於前幾天寒流農損, 本周菜價急升, 連虱目魚肚都漲 15 元. 高橋邊的豆腐攤不知何故已兩周的周日沒來擺攤, 全聯買的板豆腐根本比不上. 今年菜園產的冬瓜剩下兩個, 過完年就會用完了, 我每周滷冬瓜的貨源就沒有啦! 要幫爸重新規劃一下三餐的準備.
上週五就離開家的大咪已超過一周未回, 雖然以前也好幾次出去很久才回家的紀錄, 但會擔心牠是否能挺過上周的低溫寒流呢? 所幸今日 2/1 打電話回去時, 爸說大咪終於回來了, 這隻老貓到底還是記得回家啊!
出租的農田週六打田, 週日就插秧了, 本想下周過年可以炕土窯的說. 本季因為前陣子冬雨不斷, 導致紅豆收成不佳, 雖然價格揚升, 但產量下降收入也沒比較多啊! 務農真的是靠天吃飯, 收入不穩定風險大.
訂閱:
文章
(
Atom
)









