2016年2月4日 星期四

如何在 GAE 上佈署 jQuery EasyUI 專案 (八) : EasyUI CMS on GAE 之 3

完成整合訪客紀錄器後, 接下來要處理版面右上方 (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 (增讀修刪) 作業. 有了這組模板, 其他的功能實作起來就會比較快了.


沒有留言 :