在這個晴朗的午後讀書最好, 但最近閒書看太多了, 得趁此閒暇抓緊時間把 GAE 的資料儲存做個整理. 事實上去年此時也曾對 GAE 的資料儲存做過測試, 當時是以 ExtJS 與 jQueryUI 作為前端框架來測試 GAE Datastore 與 HTTP 要求處理, 後來因為忙股票分析系統暫時擱下了, 參考 :
# GAE 的資料儲存 (Datastore)
雖然 GAE 已被納入 Google Cloud Platform, 不像以前那像隨時申請就能上線, 但舊用戶還是能繼續免費使用, 要好好充分利用. 以下延續上篇在 GAE 上佈署 jQuery EasyUI 專案的範例, 以使用者登入介面來測試 Datastore 的基本用法. 參考 :
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (一)
# 如何在 GAE 上佈署 jQuery EasyUI 專案 (二)
首先繼承上一篇使用的 jqueryeasyui.htm 模板為 easyui_4.htm, 然後用 EasyUI 製作一個使用者登入的對話框如下 :
{% extends "jqueryeasyui.htm" %}
{% block body %}
<div id="login-dialog" class="easyui-dialog" title="系統登入" style="width:370px;height:230px;padding:10px" buttons="#login-buttons">
<div style="margin:5px;border-bottom:1px solid #ccc;">
<p>請輸入帳號密碼</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(){
$("#account").focus();
$("#login-form").form({ //設定表單
url:"/login_4",
success:function(data){
var data=eval('(' + data + ')'); //將 JSON 轉成物件
if (data.result=="success") {var msg="登入成功!";}
else {var msg="帳號或密碼錯誤!"}
$.messager.alert("訊息",msg,"info");
}
});
$("#login").bind("click",function(){
$("#login-form").submit(); //提交表單進行驗證
});
$("#cancel").bind("click",function(){
$("#login-form").form("reset"); //清除表單欄位
});
});
</script>
{% endblock %}
此登入表單的 method 設為 post, 這樣比較安全些, 然後在程式中呼叫 form() 方法來設定表單的屬性, url 是設定當按下確定鈕提交表單時向哪一個網址提出處理要求, 此處配合模板檔案 easyui_4.htm 取名為 login_4. 提交採用 Ajax 方式處理, 後端會回應一個 json 字串, 成功回應 {"result":"success"}, 失敗則回應 {"result":"failure"}, 我們用 eval() 方法將回應字串轉成物件, 即可方便地辨別成功與否, 然後用 alert 對話框輸出結果. 而按下取消鈕則會呼叫 form 的 reset 方法清除帳密.
其次是要建立一個資料儲存區來儲存使用者的帳密, 如下 model.py 所示 :
# -*- coding: utf-8 -*-
from google.appengine.ext import db
class Members(db.Model):
account=db.StringProperty()
password=db.StringProperty()
member=Members(account="admin",password="aaa")
member.put()
member=Members(account="guest",password="guest")
member.put()
首先匯入 db 類別庫, 因為需要繼承 db.Model 類別來建立我們自己的資料類別 Members, 裡面只有 account 與 password 這兩個文字欄位, 然後建立兩筆使用者資料, 最後用 put() 方法存入儲存區即可.
最後是要在主控的 main.py 中增加 easyui_4 與 login_4 的路由處理如下 :
# -*- coding: utf-8 -*-
import os
from google.appengine.ext.webapp import template
import webapp2
import model
from google.appengine.ext import db
class easyui_1(webapp2.RequestHandler):
def get(self):
url="templates/easyui_1.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class easyui_2(webapp2.RequestHandler):
def get(self):
theme=self.request.get('theme',default_value='default')
url="templates/easyui_2.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{'theme':theme})
self.response.out.write(content)
class easyui_3(webapp2.RequestHandler):
def get(self):
url="templates/easyui_3.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class easyui_4(webapp2.RequestHandler):
def get(self):
url="templates/easyui_4.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class login_4(webapp2.RequestHandler):
def post(self):
account=self.request.get("account", default_value="unknown")
password=self.request.get("password", default_value="unknown")
query=db.GqlQuery("""SELECT * FROM Members
WHERE account= :1
AND password= :2""",
account, password)
result=query.get()
if result:
content='{"result":"success"}'
else:
content='{"result":"failure","reason":"帳號或密碼錯誤"}'
self.response.out.write(content)
class MainHandler(webapp2.RequestHandler):
def get(self):
url="templates/default.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
app = webapp2.WSGIApplication([
('/', MainHandler),
('/easyui_1', easyui_1),
('/easyui_2', easyui_2),
('/easyui_3', easyui_3),
('/easyui_4', easyui_4),
('/login_4', login_4)
], debug=True)
注意這裡 login_4 類別定義的是 post 方法, 因為前端表單是用 post 方法提交的. 首先用 self.request.get() 方法取得前端傳送之帳密參數, 然後呼叫 db 的 GqlQuerry() 方法查詢 Datastore 中是否有此筆資料, 它會傳回一個查詢物件 Query, 呼叫查詢物件的 get() 方法會第一個物件實體, 若無則傳回 None, 依此即可決定要回傳給前端的 json 字串了, 最後呼叫 response.out 的 write() 方法將 json 字串傳回前端. 如下列測試 1 所示 :
測試 1 : http://jqueryeasyui.appspot.com/easyui_4
不過我發現上面這個對話框即使用了 focus() 仍無法預設聚焦在帳號輸入欄位, 不知何故? 在下列文章中有探討此問題, 以後有空再研究 :
# 利用 jQuery 將 DOM 元素聚焦 focus() 的六個版本
在之前針對 ExtJS 的測試中提到, 查詢資料儲存區還有一個方法, 即呼叫資料儲存類別 (例如此處之 Members 類別) 的 gql() 方法來查詢, 差別只在於此處須去掉 "SELECT * FROM Members", 直接用 "WHERE ..." 即可, 參考 :
# 如何在 GAE 中處理 HTTP 要求
模板檔案如下列 easyui_5.htm 所示 :
{% extends "jqueryeasyui.htm" %}
{% block body %}
<div id="login-dialog" class="easyui-dialog" 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(){
$("#account").focus();
$("#login-form").form({ //設定表單
url:"/login",
success:function(data){
var data=eval('(' + data + ')'); //將 JSON 轉成物件
if (data.result=="success") {var msg="登入成功!";}
else {var msg="帳號或密碼錯誤!"}
$("#msg").text(msg);
}
});
$("#login").bind("click",function(){
$("#login-form").submit(); //提交表單進行驗證
});
$("#cancel").bind("click",function(){
$("#login-form").form("reset"); //清除表單欄位
});
});
</script>
{% endblock %}
這與上面的 easyui_4.htm 不同的地方是顯示 Ajax 回傳結果的方式, 改在對話框最上方的文字段落 p 元素, 因此賦予它一個 id 屬性 msg, 當 Ajax 回傳結果時就呼叫包裹物件的 text() 方法更改其 innerHTML 值.
而主控程式 main.py 則添加 easyui_5 與 login_5 這兩項, 如下所示 :
# -*- coding: utf-8 -*-
import os
from google.appengine.ext.webapp import template
import webapp2
import model as m
from google.appengine.ext import db
class easyui_1(webapp2.RequestHandler):
def get(self):
url="templates/easyui_1.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class easyui_2(webapp2.RequestHandler):
def get(self):
theme=self.request.get('theme',default_value='default')
url="templates/easyui_2.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{'theme':theme})
self.response.out.write(content)
class easyui_3(webapp2.RequestHandler):
def get(self):
url="templates/easyui_3.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class easyui_4(webapp2.RequestHandler):
def get(self):
url="templates/easyui_4.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class login_4(webapp2.RequestHandler):
def post(self):
account=self.request.get("account", default_value="unknown")
password=self.request.get("password", default_value="unknown")
query=db.GqlQuery("""SELECT * FROM Members
WHERE account= :1
AND password= :2""",
account,password)
result=query.get()
if result:
content='{"result":"success"}'
else:
content='{"result":"failure","reason":"帳號或密碼錯誤"}'
self.response.out.write(content)
class easyui_5(webapp2.RequestHandler):
def get(self):
url="templates/easyui_5.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class login_5(webapp2.RequestHandler):
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:
content='{"result":"success"}'
self.response.out.write(content)
class MainHandler(webapp2.RequestHandler):
def get(self):
url="templates/default.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
app = webapp2.WSGIApplication([
('/', MainHandler),
('/easyui_1', easyui_1),
('/easyui_2', easyui_2),
('/easyui_3', easyui_3),
('/easyui_4', easyui_4),
('/login_4', login_4),
('/easyui_5', easyui_5),
('/login_5', login_5)
], debug=True)
注意這裡為了方便, 匯入 model 類別庫時我用 as 幫它取了一個別名, 這樣原本要用 model 的地方就可以用 m 代替. 其次, 判斷帳密查詢結果時, 這裡改用 is None 來判斷.
測試 2 : http://jqueryeasyui.appspot.com/easyui_5
可見現在登入結果訊息是改在上面顯示了. 實際應用上, 當登入成功後應該是重導向至網站的首頁才是, 我製作了一個首頁檔 home.htm 放在 templates 目錄下 :
{% extends "jqueryeasyui.htm" %}
{% block style %}
body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
<p>Welcome!</p>
{% endblock%}
它其實只在頁面顯示 "Welcome!" 而已. 然後修改 easyui_5.htm 為 easyui_5_1.htm 如下 :
{% extends "jqueryeasyui.htm" %}
{% block body %}
<div id="login-dialog" class="easyui-dialog" 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(){
$("#account").focus();
$("#login-form").form({ //設定表單
url:"/login_5",
success:function(data){
var data=eval('(' + data + ')'); //將 JSON 轉成物件
if (data.result=="success") {window.location.href='/home';}
else {$("#msg").text("帳號或密碼錯誤!");}
}
});
$("#login").bind("click",function(){
$("#login-form").submit(); //提交表單進行驗證
});
$("#cancel").bind("click",function(){
$("#login-form").form("reset"); //清除表單欄位
});
});
</script>
{% endblock %}
這裡後端核對帳密部分仍然使用 login_5, 我只修改了 Ajax 傳回值處理部分, 失敗時仍然在對話框上面顯示錯誤訊息, 成功時就將網頁導向路徑 /home, 也就是 home.htm 模板. 當然 main.py 也要修改如下 :
# -*- coding: utf-8 -*-
import os
from google.appengine.ext.webapp import template
import webapp2
import model as m
from google.appengine.ext import db
class easyui_1(webapp2.RequestHandler):
def get(self):
url="templates/easyui_1.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class easyui_2(webapp2.RequestHandler):
def get(self):
theme=self.request.get('theme',default_value='default')
url="templates/easyui_2.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{'theme':theme})
self.response.out.write(content)
class easyui_3(webapp2.RequestHandler):
def get(self):
url="templates/easyui_3.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class easyui_4(webapp2.RequestHandler):
def get(self):
url="templates/easyui_4.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class login_4(webapp2.RequestHandler):
def post(self):
account=self.request.get("account", default_value="unknown")
password=self.request.get("password", default_value="unknown")
query=db.GqlQuery("""SELECT * FROM Members
WHERE account= :1
AND password= :2""",
account,password)
result=query.get()
if result:
content='{"result":"success"}'
else:
content='{"result":"failure","reason":"帳號或密碼錯誤"}'
self.response.out.write(content)
class easyui_5(webapp2.RequestHandler):
def get(self):
url="templates/easyui_5.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class login_5(webapp2.RequestHandler):
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:
content='{"result":"success"}'
self.response.out.write(content)
class easyui_5_1(webapp2.RequestHandler):
def get(self):
url="templates/easyui_5_1.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class home(webapp2.RequestHandler):
def get(self):
url="templates/home.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
class MainHandler(webapp2.RequestHandler):
def get(self):
url="templates/default.htm"
path=os.path.join(os.path.dirname(__file__), url)
content=template.render(path,{})
self.response.out.write(content)
app = webapp2.WSGIApplication([
('/', MainHandler),
('/easyui_1', easyui_1),
('/easyui_2', easyui_2),
('/easyui_3', easyui_3),
('/easyui_4', easyui_4),
('/login_4', login_4),
('/easyui_5', easyui_5),
('/login_5', login_5),
('/easyui_5_1', easyui_5_1),
('/home', home)
], debug=True)
也就是添加 easyui_5_1 與 home 這兩個路徑.
測試 3 : http://jqueryeasyui.appspot.com/easyui_5_1
測試結果正確. 今天就做到這裡了. 以上範例程式碼可在下列網址下載 :
# 下載原始碼
沒有留言:
張貼留言