2016年1月1日 星期五

如何在 GAE 上佈署 jQuery EasyUI 專案 (三) : 會員登入

今天是 2016 年的第一天, 早上睡到八點才起床, 因為昨晚熬到 12:00 的跨年煙火秀 (看電視的啦) 才睡覺, 但睡前忘了關閉手機的鬧鐘, 六點被吵起來後繼續睡. 早上瞎忙一陣到 11 點才出發回鄉下, 國十接近轉南二高路段又塞車, 結果回到家已經 12 點了.

在這個晴朗的午後讀書最好, 但最近閒書看太多了, 得趁此閒暇抓緊時間把 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 

測試結果正確. 今天就做到這裡了. 以上範例程式碼可在下列網址下載 :

# 下載原始碼


沒有留言:

張貼留言