2016年2月28日 星期日

如何在 GAE 上佈署 jQuery EasyUI 專案 (十一) : EasyUI CMS on GAE 之 6

原以為 EasyUI  on GAE 可以告一段落了, 想整理檔案上傳雲端保存, 卻發現我漏掉一個功能沒有實作, 就是使用者管理. 有前面其他功能當範本, 要實作也是很快, 今天斷斷續續修改測試一下就搞定了, 以下是測試紀錄.

首先是去 model.py 修改之前做登入測試時就建立的 Members 資料模型, 添加了 email (預備發 email 用), 與 mobile (預備發簡訊用) 兩個欄位如下 :

class Members(db.Model):
    account=db.StringProperty(required=True)
    password=db.StringProperty(required=True)
    theme=db.StringProperty(required=True,default="default")
    is_admin=db.BooleanProperty(required=True,default=False)
    email=db.EmailProperty(required=False)
    mobile=db.StringProperty(required=False)

member=Members(key_name="admin",account="admin",password="admin",
    theme="default",is_admin=True,email="foo@bar.com",mobile="0933")
member.put()
member=Members(key_name="guest",account="guest",password="guest",
    theme="black",is_admin=False,email="foo@bar.com",mobile="0932")
member.put()

其次新增一個 Systabs 資料模型的實體, 以便增加一個 "使用者" 頁籤, 超連結指向 /list_members 這個路徑 :

systab=Systabs(key_name="list_members",tab_name="list_members",
    tab_label=u"使用者",tab_link="/list_members",tab_order=3,tab_admin=True)
systab.put()

這樣就搞定資料儲存了, 接下來去 main.py 新增 /list_members 路徑的處理類別, 主要是用來顯示目前的使用者列表 :

class list_members(webapp2.RequestHandler):
    def get(self):
        #query Themes from datastore
        themes=m.Themes.all()
        info={}
        theme_list=[]
        for t in themes:
            theme_list.append(t.theme)
        info["themes"]=theme_list
        url="templates/list_members.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{'info':info})
        self.response.out.write(content)

可見它會渲染 templates 下的 list_members.htm 這個網頁模板, 並且將全部主題布景選項放在字典物件中傳遞給它, 以便在新增使用者頁面中產生下拉式選單. 此模板 list_members.htm 內容如下 :

{% extends "jqueryeasyui.htm" %}
{% block style %}
  body {font: 80% "Trebuchet MS", sans-serif; margin: 50px;}
{% endblock%}
{% block body %}
  <!--使用者 Members 列表-->
  <table id="members" title="使用者列表" style="width:auto" data-options="tools:'#members_tools'"></table>
  <div id="members_tools">  
    <a href="#" id="add_member" class="icon-add" title="新增"></a>
    <a href="#" id="edit_member" class="icon-edit" title="編輯"></a>
    <a href="#" id="remove_member" class="icon-remove" title="刪除"></a>
    <a href="#" id="reload_members" class="icon-reload" title="重新載入"></a>
  </div>
  <!--新增&編輯 Members 表單對話框-->
  <div id="member_dialog" class="easyui-dialog" title="新增連結" style="width:360px;height:270px;" data-options="closed:'true',buttons:'#member_buttons'">
    <form id="member_form" method="post" style="padding:10px">
      <div style="margin:5px">
        <label style="width:60px;display:inline-block;">帳號 : </label>
        <input id="member_account" name="account" 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="password" 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="theme" id="user_theme" class="easyui-combobox" data-options="required:true,panelHeight:'auto'">
{% for t in info.themes %}
              <option value="{{t}}">{{t}}</option>
{% endfor %}
        </select>
      </div>
      <div style="margin:5px">
        <label style="width:60px;display:inline-block;">管理員 : </label>
        <select name="is_admin" id="is_admin" class="easyui-combobox" data-options="required:true,panelHeight:'auto'">
          <option value="True">True</option>
          <option value="False">False</option>
        </select>
      </div>
      <div style="margin:5px">
        <label style="width:60px;display:inline-block;">Email : </label>
        <input name="email" id="email" type="text" class="easyui-textbox" data-options="required:true"  style="width:230px">
      </div>
      <div style="margin:5px">
        <label style="width:60px;display:inline-block;">行動電話 : </label>
        <input name="mobile" type="text" class="easyui-textbox" style="width:230px">
        <input type="hidden" id="member_op" value="">
      </div>
    </form>
  </div>
  <div id="member_buttons" style="padding-right:15px;">
    <a href="#" id="clear_member" class="easyui-linkbutton" iconCls="icon-clear" style="width:90px">重設</a>
    <a href="#" id="save_member" class="easyui-linkbutton" iconCls="icon-ok" style="width:90px">確定</a>
  </div>
  <script>
    $(function(){
      //標頭連結 members
      $('#members').datagrid({
        columns:[[
          {field:'account',title:'帳號',sortable:true},
          {field:'password',title:'密碼',sortable:true},
          {field:'theme',title:'主題布景',sortable:true},
          {field:'is_admin',title:'管理員',sortable:true},
          {field:'email',title:'Email',sortable:true},
          {field:'mobile',title:'行動電話',sortable:true}
          ]],
        url:"/get_members",
        method:"post",
        singleSelect:true,
        rownumbers:true
        });
      $("#clear_member").bind("click",function(){
        $("#member_form")[0].reset();
        });
      $("#save_member").bind("click",function(){      
        var op=$("#member_op").val();  //判斷是新增或修改
        if (op=="update") {var url="/update_member";}
        else {var url="/add_member";}
        $("#member_form").form("submit",{
          url:url,
          method:"post",
          success:function(data){
            var data=eval('(' + data + ')');
            $("#member_dialog").dialog("close");
            if (data.status==="success") {
              $("#members").datagrid("reload");
              }
            else {
              $.messager.alert("訊息",data.reason,"error");  
              }        
            }
          });
        });
      $("#add_member").bind("click",function(){
        $("#member_dialog").dialog("open").dialog("setTitle","新增使用者");
        $("#member_account").textbox({"readonly":false}); //for adding
        $("#member_form").form("clear");
        $("#member_op").val("add");
        $('#user_theme').combobox();
        $('#user_theme').combobox('setValue','default');
        $('#is_admin').combobox();
        $('#is_admin').combobox('setValue','False');
        $('#email').textbox('setValue','foo@bar.com');
        });
      $("#edit_member").bind("click",function(){
        var row=$("#members").datagrid("getSelected");
        if (row) {
          $("#member_dialog").dialog("open").dialog("setTitle","編輯使用者");
          $("#member_form").form("load",row);
          $("#member_account").textbox({"readonly":true});
          $("#member_op").val("update");
          }
        else {$.messager.alert("訊息","請先選取要編輯的使用者!","error");}
        });
      $("#remove_member").bind("click",function(){
        var row=$("#members").datagrid("getSelected");
        if (row) {
          var params={account:row.account};
          $.messager.confirm("確認","確定要刪除這個使用者嗎?",function(btn){
            if (btn){
              if (row.account=="admin") {
                $.messager.alert("訊息","此為系統管理者不可刪除!","error");
                return;
                }
              var callback=function(data){
                if (data.status==="success"){
                  $("#members").datagrid("reload");
                  }
                else {$.messager.alert("訊息",data.reason,"error");}          
                };              
              $.post("/remove_member",params,callback,"json");
              }
            })
          }
        });
      $("#reload_members").bind("click",function(){
        $("#members").datagrid("load");
        });
      });
  </script>
{% endblock%}

這裡黃顏色的部分是今天花比較多時間磨合的地方. email 這個欄位很奇怪, 我明明在 model.py 中已經用 required=False 指名此欄位不必須填值, 但測試新增使用者時卻還是報錯, 說 email 不可 empty. 沒辦法, 乾脆在 data-options 中將 email 設為必填. 既然如此, 為了用 Easyui 的 setValue 設定初始值, 就幫此欄位多設一個 id 屬性.

另外, 要幫 theme 與 is_admin 這兩個下拉式選單設定初始值時發現, 直接用 combobox 的 setValue 方法是不行的, 會說找不到 options 物件, 必須先呼叫 combobox() 方法初始化物件, 再呼叫 setValue 方法才會生效.

值得一提的是, 我利用一個隱藏欄位 member_op 來儲存是要 add 還是 update 使用者資料, 如果是編輯動作, 就將 account 欄位設為唯讀 (帳號一經建立不能修改, 只能刪除), 並將 member_op 設為 update; 若為新增動作, 就清除 account 欄位的唯讀屬性, 並將 member_op 設為 add, 這樣就可以共用一張表單了.

此 datagrid 是從 /get_members 路徑取得資料來源, 其路徑處理類別如下 :

class get_members(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="account"
        if not len(order):
            order="asc"
        query=m.Members.gql("ORDER BY %s %s" % (sort, order))
        count=query.count()
        mbs=query.fetch(rows, (page-1)*rows)
        rows=[]  #for storing objects
        for mb in mbs:
            member={"account":mb.account,
                    "password":mb.password,
                    "theme":mb.theme,
                    "is_admin":mb.is_admin,
                    "email":mb.email,
                    "mobile":mb.mobile}
            rows.append(member)
        obj={"total":count,"rows":rows}  #Easyui datagrid json format
        self.response.headers["Content-Type"]="application/json"
        self.response.out.write(json.dumps(obj))

就是讀取全部 Members 資料實體, 將各欄位值編成 json 傳回前端而已. 

新增使用者 /add_member 的路徑處理類別如下 :

class add_member(webapp2.RequestHandler):
    def post(self):
        account=self.request.get("account")
        password=self.request.get("password")
        theme=self.request.get("theme")
        is_admin=self.request.get("is_admin")
        email=self.request.get("email", default_value="foo@bar.com")
        mobile=self.request.get("mobile", default_value="0933123456")
        #trans string to boolean
        if is_admin=="True":
            is_admin=True
        else:
            is_admin=False
        #check entity if exist
        mb=m.Members.get_by_key_name(account)
        if mb: #already exist
            result='{"status":"failure","reason":"帳號已存在!"}'  
        else:  #new member
            member=m.Members(key_name=account,
                account=account,
                password=password,
                theme=theme,
                is_admin=is_admin,
                email=email,
                mobile=mobile
                )
            member.put()
            result='{"status":"success"}'         
        self.response.out.write(result) 

這裡要注意的是資料類型為 boolean 的 is_admin 欄位, 因為前端傳出的 True 與 False 都是字串, 必須轉換為 boolean 欄位才行, 否則會出現資料不符錯誤. 

更新 /update_member 與移除 /remove_member 的處理類別如下 : 

class update_member(webapp2.RequestHandler):
    def post(self):
        account=self.request.get("account")
        password=self.request.get("password")
        theme=self.request.get("theme")
        is_admin=self.request.get("is_admin")
        email=self.request.get("email")
        mobile=self.request.get("mobile", default_value="")
        #trans string to boolean
        if is_admin=="True":
            is_admin=True
        else:
            is_admin=False
        #get entity from store
        mb=m.Members.get_by_key_name(account)
        if mb: #entity exist
            mb.account=account
            mb.password=password
            mb.theme=theme
            mb.is_admin=is_admin
            mb.email=email
            mb.mobile=mobile
            mb.put()
            result='{"status":"success"}'
        else:  #member not existed
            result='{"status":"failure","reason":"使用者不存在!"}'         
        self.response.out.write(result)

class remove_member(webapp2.RequestHandler):
    def post(self):
        account=self.request.get("account")
        #get entity from store
        mb=m.Members.get_by_key_name(account)
        if mb: #entity exist
            db.delete(mb)            
            result='{"status":"success"}'
        else:  #member not existed
            result='{"status":"failure","reason":"使用者不存在!"}'         
        self.response.out.write(result) 

以上便是整個使用者管理的完整 CRUD 實作紀錄, 實際測試範例如下 (登入帳密 admin, admin 或 guest, guest) :






OK, 終於完成 EasyuiCMS on GAE 的全部的系統基本管理功能了, 包括登入登出, 使用者管理, 導覽列管理, 標頭超連結管理, 訪客紀錄器. 其實還有一個 App 管理, 需要檔案上傳功能, 但是 GAE 與一般 PHP 應用不同, 我不太確定 GAE 能照搬 PHP 版本過來, 直覺是必須跟系統寫在一起 (擴增功能), 可能很難獨立開來, 但或許也有辦法, 只是還沒想出來而已.


沒有留言 :