# 如何在 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.
沒有留言 :
張貼留言