2016年3月2日 星期三

EasyuiCMS on GAE 總整理

過去兩個月嘗試把一年前用 PHP 寫的 EasyuiCMS 移植到 GAE 上, 終於有了初步成果, 自從 2009 年底開始接觸 Google 的雲端主機服務 GAE 以來, 就有在 GAE 上寫一個簡單的架站系統的想法, 這樣我就可以很容易地在上面寫各種應用服務程式了. 這幾年大部分時間都花在 PHP 與 Java 的學習上, 對於 Python 一直沒有做系統化的學習, 在 GAE 上用 Python 也是見招拆招而已.

去年底休假時無意中看到以前寫的 GAE 程式, 就突然想動手試試看是否真能在 GAE 上寫個 CMS, 沒想到一寫就不能罷手, 至今整整兩個月總算有了小收穫, 把猜想化為可能, 今天我把這些結果重新整理, 刪除多餘的測試部分, 打包起來存檔, 以備後用.

目錄結構如下所示, /static/easyui 下放置 Easyui 函式庫, /static/images 下放置系統所需圖檔, 而 /templates 下則放置網頁模板 :


應用程式設置檔 app.yaml 內容如下 :

application: gaeweb1
version: 1
runtime: python27
api_version: 1
threadsafe: yes

handlers:
- url: /favicon\.ico
  static_files: favicon.ico
  upload: favicon\.ico
- url: /static
  static_dir: static
- url: .*
  script: main.app

libraries:
- name: webapp2
  version: "2.5.2"

這裡唯一需要修改的部分是第一個參數 application, 其中 gaeweb1 是我所申請的 GAE 應用程式帳號, 如果要把這 CMS 用在別的帳號, 只要改掉 application 之值即可.

其次是資料模型程式 model.py, 其內容如下 :

# -*- coding: utf-8 -*-
from google.appengine.ext import db

class Members(db.Model):
    account=db.StringProperty(required=True)
    password=db.StringProperty(required=True)
    name=db.StringProperty(required=False)
    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)
    login_count=db.IntegerProperty(default=0)
    last_login_time=db.DateTimeProperty(auto_now_add=True)

class Visitors(db.Model):
    ip=db.StringProperty()
    visit_time=db.DateTimeProperty()
    user_agent=db.StringProperty()

class Settings(db.Model):
    site_title=db.StringProperty()
    site_theme=db.StringProperty()
    site_state=db.StringProperty()
    site_created_time=db.DateTimeProperty(auto_now_add=True)

class Board(db.Model):
    poster=db.StringProperty()
    subject=db.StringProperty()
    content=db.TextProperty()
    post_time=db.DateTimeProperty()

class Themes(db.Model):
    theme=db.StringProperty()

class Hometabs(db.Model):
    tab_name=db.StringProperty()
    tab_label=db.StringProperty()
    tab_link=db.StringProperty()
    tab_tip=db.StringProperty()
    tab_order=db.IntegerProperty()
    tab_admin=db.BooleanProperty()

class Headerlinks(db.Model):
    name=db.StringProperty()
    title=db.StringProperty()
    url=db.StringProperty()
    target=db.StringProperty()
    sequence=db.IntegerProperty()
    hint=db.StringProperty()

class Navblocks(db.Model):
    name=db.StringProperty()
    title=db.StringProperty()
    sequence=db.IntegerProperty()
    display=db.BooleanProperty()

class Navlinks(db.Model):
    name=db.StringProperty()
    title=db.StringProperty()
    url=db.StringProperty()
    target=db.StringProperty()
    sequence=db.IntegerProperty()
    block_name=db.StringProperty()
    hint=db.StringProperty()

settings=Settings(key_name="settings",site_title="EasyUI-based CMS on GAE",
    site_theme="default",site_state="on")
settings.put()

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

theme=Themes(key_name="default",theme="default")
theme.put()
theme=Themes(key_name="gray",theme="gray")
theme.put()
theme=Themes(key_name="black",theme="black")
theme.put()
theme=Themes(key_name="bootstrap",theme="bootstrap")
theme.put()
theme=Themes(key_name="metro",theme="metro")
theme.put()
theme=Themes(key_name="metro-blue",theme="metro-blue")
theme.put()
theme=Themes(key_name="metro-gray",theme="metro-gray")
theme.put()
theme=Themes(key_name="metro-green",theme="metro-green")
theme.put()
theme=Themes(key_name="metro-orange",theme="metro-orange")
theme.put()
theme=Themes(key_name="metro-red",theme="metro-red")
theme.put()
theme=Themes(key_name="ui-cupertino",theme="ui-cupertino")
theme.put()
theme=Themes(key_name="ui-dark-hive",theme="ui-dark-hive")
theme.put()
theme=Themes(key_name="ui-pepper-grinder",theme="ui-pepper-grinder")
theme.put()
theme=Themes(key_name="ui-sunny",theme="ui-sunny")
theme.put()

hometab=Hometabs(key_name="home",tab_name="home",tab_label=u"首頁",
    tab_link="/home",tab_order=0,tab_admin=False)
hometab.put()
hometab=Hometabs(key_name="member_settings",tab_name="member_settings", tab_label=u"使用者設定",tab_link="/member_settings",tab_order=1,tab_admin=False)
hometab.put()
hometab=Hometabs(key_name="list_visitors",tab_name="list_visitors",
    tab_label=u"訪客",tab_link="/list_visitors",tab_order=2,tab_admin=True)
hometab.put()
hometab=Hometabs(key_name="list_members",tab_name="list_members",
    tab_label=u"使用者",tab_link="/list_members",tab_order=3,tab_admin=True)
hometab.put()
hometab=Hometabs(key_name="list_headerlinks",tab_name="list_headerlinks",
    tab_label=u"標頭連結",tab_link="/list_headerlinks",tab_order=4,tab_admin=True)
hometab.put()
hometab=Hometabs(key_name="list_navblocks",tab_name="list_navblocks",
    tab_label=u"導覽區塊",tab_link="/list_navblocks",tab_order=5,tab_admin=True)
hometab.put()
hometab=Hometabs(key_name="list_navlinks",tab_name="list_navlinks",
    tab_label=u"導覽連結",tab_link="/list_navlinks",tab_order=6,tab_admin=True)
hometab.put()
hometab=Hometabs(key_name="settings",tab_name="settings",
    tab_label=u"系統設定",tab_link="/settings",tab_order=7,tab_admin=True)
hometab.put()

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()

navblock=Navblocks(key_name="main",name="main",title=u"主功能",
    sequence=0,display=True)
navblock.put()

navlink=Navlinks(key_name="home",name="home",title=u"首頁",
    url="javascript:gohome()",target="_self",sequence=0,block_name="main",
    hint=u"首頁")
navlink.put()
navlink=Navlinks(key_name="logout",name="logout",title=u"登出",
    url="javascript:logout()",target="_self",sequence=0,block_name="main",
    hint=u"登出")
navlink.put()

這裡我給 Members 添加了 login_count 與 last_login_time 這兩個欄位來記錄使用者登入次數與最近一次登入時間, 在 main.py 中處理登入時會累計登入次數並更新登入時間. 其所渲染的 list_members.htm 也配合修改如下 :

      $('#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},
          {field:'login_count',title:'登入次數',sortable:true},
          {field:'last_login_time',title:'最近登入',sortable:true}
          ]],
      url:"/get_members",
      method:"post",
      singleSelect:true,
      fitColumns:true,
      rownumbers:true,
      pagination:true,
      pageSize:10
      });


另外也拿掉以前測試 Visitors 時為了全文搜尋而加的 ip_list 與 user_agent_list 欄位, 僅做 "search_what%" 半模糊比對 (即僅開頭符合), 而且搜尋框也改用 textbox 內含 icon 方式. 其次是將原來的 Systabs 改為 Hometabs, 因為既然回首頁按鈕為呼叫 gohome(), 而且首頁就在此頁籤架構中, 稱為 Hometabs 較名符其實. 當然主程式 main.py 也要配合修改.

在 main.py 中我做了蠻大修訂, 首先為了安全性考量, 我在 BaseHandler 類別中增加一個 check_login() 的函式讓各路徑處理類別在一開始時呼叫, 以檢驗是否已建立連線 Session 物件, 沒有的話一律重導向至登入頁面, 避免使用者直接存取各 url 路徑. 當然, 各路徑處理類別除了 login 外都改為繼承 BaseHandler 而非 webapp2.RequestHandler, 因為要用到 Session 機制之故 (其實 BaseHandler 是 RequestHandler 的子類別) :

# -*- coding: utf-8 -*-
import os
import json
import datetime
import webapp2
import model as m
from datetime import timedelta
from webapp2_extras import sessions
from google.appengine.ext import db
from google.appengine.ext.webapp import template

class BaseHandler(webapp2.RequestHandler):
    def dispatch(self):
        # Get a session store for this request.
        self.session_store=sessions.get_store(request=self.request)
        try:
            #Dispatch the request.
            webapp2.RequestHandler.dispatch(self)
        finally:
            #Save all sessions.
            self.session_store.save_sessions(self.response)
    @webapp2.cached_property
    def session(self):
        #Returns a session using the default cookie key.
        sess=self.session_store.get_session()
        #add some default values:
        if not sess.get("theme"):
            sess["theme"]="default"
        return sess
    def check_login(self):
        account=self.session.get('account')
        if account is None:
            self.redirect("/login")

登入登出的路徑處理類別如下 :

class login(webapp2.RequestHandler):
    def get(self):
        s=m.Settings.get_by_key_name("settings")
        info={}
        if s is None:
            info["site_title"]=""
            info["site_theme"]="ui-cupertino"
        else:
            info["site_title"]=s.site_title
            info["site_theme"]=s.site_theme
        url="templates/login.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{"info":info})
        self.response.out.write(content)

class check_member(BaseHandler):
    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)
        mb=query.get()
        if mb is None: #user not existed
            content='{"result":"failure","reason":"帳號或密碼錯誤"}'
        else: #user existed
            #check site_state (on/off)
            s=m.Settings.get_by_key_name("settings")
            site_state=s.site_state
            #go main page if system is on or user is admin
            if site_state=="on" or mb.is_admin:
                #save session
                self.session['account']=mb.account     
                self.session['theme']=mb.theme         
                self.session['is_admin']=mb.is_admin
                #update last_login_time & login_count of the user
                mb.last_login_time=datetime.datetime.now() + timedelta(hours=+8)
                mb.login_count=mb.login_count + 1
                mb.put()
                content='{"result":"success"}'
            else: #system is on maintanance (off) and not admin
                content='{"result":"failure","reason":"系統維護中, 請稍候."}'
        self.response.out.write(content)

class logout(BaseHandler):
    def get(self):
        #clear session & back to login
        self.session.clear()
        self.redirect("/login")

在 login 類別中會將 site_theme 傳入 login.htm 中, 用來更改此網頁的主題布景, 這裡我改用字串處理方式來製作 css 檔的路徑, 這樣不管 easyui 是使用 CDN 與自行供檔都會產生所需之路徑. 注意, 在 check_member 類別中, 若登入成功, 會將 login_count 欄位增量 1, 且更新 last_login_time 欄位. 而當 sys_settings 資料表中的 site_state 被設為 off, 且使用者不是管理員時, 系統不會讓使用者登入, 登入檢查會失敗, 並回應系統維護中訊息.



另外我把原先的 login_check 類別改名為 check_member, 因為那很容易跟 BaseHandler 類別裡的 check_login() 函式搞混. 當然, login.htm 中登入網頁的路徑也要同步修改 :

      //將主題布景改為系統設定
      var href=$("#theme").attr("href");
      var css="themes/{{info.site_theme}}/easyui.css"
      href=href.substr(0,href.indexOf("themes")) + css;
      $("#theme").attr("href", href);
      $("#login-form").form({ //設定表單
        url:"/check_member",
        success:function(data){
          var data=eval('(' + data + ')');  //將 JSON 轉成物件
          if (data.result=="success") {window.location.href='/';}
          else {$("#msg").text(data.reason);}
          }
        });

下面是更改個人主題布景的路徑處理類別 :

class change_theme(BaseHandler):
    def get(self):
        self.check_login()
        theme=self.request.get("theme")  #get selected theme
        self.session['theme']=theme  #update user session
        account=self.session.get('account')
        member=m.Members.get_by_key_name(account)  #retrieve entity
        member.theme=theme
        member.put()  #update user theme in datastre

首頁頁籤架構處理 :

class hometabs(BaseHandler):
    def get(self):
        self.check_login()
        hometabs=m.Hometabs.all()
        hometabs.order("tab_order")  #sort by tab_order
        tabs=[]  #for storing tab objects
        is_admin=self.session.get('is_admin')  #True/False
        for t in hometabs:
            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/hometabs.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{"tabs":tabs})
        self.response.out.write(content)

class home(BaseHandler):
    def get(self):
        self.check_login()
        url="templates/home.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

這個首頁 home.htm 也是維持僅顯示 "Welcome" 字樣, 寫應用程式時再視需要修改即可 (例如顯示應用服務的 Dashboard 畫面或服務摘要表等).

訪客紀錄器類別如下 :

class list_visitors(BaseHandler):
    def get(self):
        self.check_login()
        url="templates/list_visitors.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class get_visitors(BaseHandler):
    def post(self):
        self.check_login()
        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):
            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))

class remove_visitors(BaseHandler):
    def post(self):
        self.check_login()
        visitors=m.Visitors.all()
        for v in visitors:
            db.delete(v)
        content='{"status":"success"}'          
        self.response.out.write(content)

因為在 model.py 中我已經拿掉 ip_list 與 user_agent_list 欄位, 因此這裡的資料實體搜尋也去掉對此兩欄位的全文搜尋, 改為 "search_what%" 的前面符合搜尋. 而它所渲染的 list_visitors.htm 網頁, 其中下拉式選單的選項值也要修改, 將 ip_list 與 user_agent_list 改為 ip 與 user_agent 如下 :

{% 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_visitors" 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>
      <option value="user_agent">瀏覽器</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 位址',width:150,sortable:true},
          {field:'visit_time',title:'到訪時間',width:150,sortable:true},
          {field:'user_agent',title:'瀏覽器',sortable:true}
          ]],
        singleSelect:true,
        fitColumns:true,
        collapsible:true,
        rownumbers:true,
        pagination:true,
        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",{search_field:""});
        });
      $("#remove_visitors").bind("click",function(){
        $.messager.confirm("確認","確定要刪除全部訪客紀錄嗎?",function(btn){
          if (btn){
            var params={};
            var callback=function(data){
              if (data.status==="success"){$("#visitors").datagrid("reload");}
              else {$.messager.alert("訊息","刪除訪客紀錄失敗!","error");}
              }
            $.post("/remove_visitors",params,callback,"json");
            }
          });
        });
      });
  </script>
{% endblock%}


此處在搜尋框 search_what 中添加 iconCls:"icon-search" 就會在文字框內出現一個放大鏡搜尋圖樣按鈕. 當按右上角的 reload 按鈕時, 會觸發 datagrid 重新載入, 但是若之前有搜尋動作, 那麼重載時仍會傳送上次的 search_field 參數, 導致不是載入全部資料, 而是與上次搜尋相同的結果, 與預期動作不符. 解決之道是傳入一個空字串的 search_field 參數即可.

其次是我把 remove_visitor 類別改為 remove_visitors, 主要考量是 GAE 不像 MySQL 那樣可用一個自動增量主鍵 id 簡單鎖定要刪除的那筆紀錄, 雖然可用 key_name 來做, 但每一筆訪客紀錄都要設 key_name 也很麻煩, 只能使用日期時間, 但麻煩的是資料庫裡儲存的是精確的日期時間類型, 但網頁顯示的只到秒, 導致從 datagrid 點選一筆要刪除時根本無法真正將資料實體刪除, 因為在搜尋時找不到那筆實體. 當然可以乾脆將 visit_time 改為 StringProperty 類型來解決, 但是想到訪客紀錄有需要一筆一筆殺嗎? 它只是用來看看有哪些人來瀏覽而已, 看完就清空即可, 所以這裡 remove_visitors 類別是刪除全部訪客紀錄實體.

下面是使用者管理相關路徑處理類別, 與上一篇文章內容差不多, 只是改為繼承 BaseHandler 與加入 check_login() :

class list_members(BaseHandler):
    def get(self):
        self.check_login()
        #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)

class get_members(BaseHandler):
    def post(self):
        self.check_login()
        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))

class add_member(BaseHandler):
    def post(self):
        self.check_login()
        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)

class update_member(BaseHandler):
    def post(self):
        self.check_login()
        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(BaseHandler):
    def post(self):
        self.check_login()
        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)

下面是標頭超連結管理相關之路徑處理類別 :

class list_headerlinks(BaseHandler):
    def get(self):
        self.check_login()
        url="templates/list_headerlinks.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class get_headerlinks(BaseHandler):
    def post(self):
        self.check_login()
        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))

class add_headerlink(BaseHandler):
    def post(self):
        self.check_login()
        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)

class update_headerlink(BaseHandler):
    def post(self):
        self.check_login()
        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)

class remove_headerlink(BaseHandler):
    def post(self):
        self.check_login()
        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)

下列是導覽區塊管理相關之路徑處理類別 :

class list_navblocks(BaseHandler):
    def get(self):
        self.check_login()
        url="templates/list_navblocks.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{})
        self.response.out.write(content)

class get_navblocks(BaseHandler):
    def post(self):
        self.check_login()
        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.Navblocks.gql("ORDER BY %s %s" % (sort, order))
        count=query.count()
        blocks=query.fetch(rows, (page-1)*rows)
        rows=[]  #for storing objects
        for b in blocks:
            block={"name":b.name,
                   "title":b.title,
                   "sequence":b.sequence,
                   "display":b.display}
            rows.append(block)
        obj={"total":count,"rows":rows}  #Easyui datagrid json format
        self.response.headers["Content-Type"]="application/json"
        self.response.out.write(json.dumps(obj))

class add_navblock(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        display=self.request.get("display")
        if display=="True":
            display=True
        else:
            display=False
        #check entity if exist
        block=m.Navblocks.get_by_key_name(name)
        if block: #already exist
            result='{"status":"failure","reason":"導覽區塊已存在!"}'
        else:  #new block
            navblock=m.Navblocks(key_name=name,name=name,
                title=self.request.get("title"),
                sequence=int(self.request.get("sequence")),
                display=display
                )
            navblock.put()
            result='{"status":"success"}'      
        self.response.out.write(result)

class update_navblock(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        display=self.request.get("display")
        if display=="True":
            display=True
        else:
            display=False
        #get entity from store
        block=m.Navblocks.get_by_key_name(name)
        if block: #entity exist
            block.title=self.request.get("title")
            block.sequence=int(self.request.get("sequence"))
            block.display=display
            block.put()
            result='{"status":"success"}'
        else:  #block not existed
            result='{"status":"failure","reason":"導覽區塊不存在!"}'      
        self.response.out.write(result)

class remove_navblock(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        #get entity from store
        block=m.Navblocks.get_by_key_name(name)
        if block: #entity exist
            db.delete(block)
            #delete navlinks belong to this navblock
            query=m.Navlinks.all()
            links=query.filter("block_name",name)
            for link in links:
                db.delete(link)
            result='{"status":"success"}'
        else:  #block not existed
            result='{"status":"failure","reason":"導覽區塊不存在!"}'      
        self.response.out.write(result)

下面是導覽列超連結管理相關之路徑處理類別 :

class list_navlinks(BaseHandler):
    def get(self):
        self.check_login()
        blocks=m.Navblocks.all()
        info=[]
        for b in blocks:
            block={}
            block["block_name"]=b.name
            block["block_title"]=b.title
            #block["block_title"]=b.title + " (" + b.name + ")"
            info.append(block)
        url="templates/list_navlinks.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{"info":info})
        self.response.out.write(content)

class get_navlinks(BaseHandler):
    def post(self):
        self.check_login()
        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.Navlinks.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,
                  "block_name":h.block_name,
                  "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))

class add_navlink(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        #check entity if exist
        link=m.Navlinks.get_by_key_name(name)
        if link: #already exist
            result='{"status":"failure","reason":"連結名稱已存在!"}'
        else:  #new link
            navlink=m.Navlinks(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")),
                block_name=self.request.get("block_name"),
                hint=self.request.get("hint")
                )
            navlink.put()
            result='{"status":"success"}'      
        self.response.out.write(result)

class update_navlink(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        #get entity from store
        link=m.Navlinks.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.block_name=self.request.get("block_name")
            link.hint=self.request.get("hint")
            link.put()
            result='{"status":"success"}'
        else:  #link not existed
            result='{"status":"failure","reason":"連結不存在!"}'      
        self.response.out.write(result)

class remove_navlink(BaseHandler):
    def post(self):
        self.check_login()
        name=self.request.get("name")
        #get entity from store
        link=m.Navlinks.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)

下面是系統設定管理相關之路徑處理類別 :

class settings(BaseHandler):
    def get(self):
        self.check_login()
        info={}  #for storing parameters
        #query settings from datastore
        settings=m.Settings.get_by_key_name("settings")
        if settings: #entity exist
            info["site_title"]=settings.site_title
            info["site_theme"]=settings.site_theme
            info["site_state"]=settings.site_state
        else:
            info["site_title"]=""
            info["site_theme"]="default"
            info["site_state"]="off"
        #query Themes from datastore
        themes=m.Themes.all()
        theme_list=[]
        for t in themes:
            theme_list.append(t.theme)
        info["themes"]=theme_list
        url="templates/settings.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{'info':info})
        self.response.out.write(content)

class update_settings(BaseHandler):
    def post(self):
        self.check_login()
        #get entity from store
        settings=m.Settings.get_by_key_name("settings")
        if settings: #entity exist
            settings.site_title=self.request.get("site_title")
            settings.site_theme=self.request.get("site_theme")
            settings.site_state=self.request.get("site_state")
            settings.put()
            result='{"status":"success"}'
        else:  #entity not existed
            result='{"status":"failure"}'      
        self.response.out.write(result)

最後是根目錄處理類別與路徑處理指派 :

class MainHandler(BaseHandler):
    def get(self):
        #save visitor info first
        ip=self.request.remote_addr
        user_agent=self.request.headers.get("User-Agent")
        # or user_agent=os.environ.get("HTTP_USER_AGENT")
        visitor=m.Visitors()
        visitor.ip=ip
        #adapt UTC to Taipei Time Zone
        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
        self.check_login()
        #pass: already login, render main.htm
        info={}  #for storing parameters
        #create param: account, site_title, theme
        account=self.session.get('account')
        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
        #create param: navblocks & navlinks
        navblocks=m.Navblocks.all()
        navblocks.filter("display =",True)
        navblocks.order("sequence")  #sort by sequence          
        navblock_list=[]  #for storing navblocks objects
        for nb in navblocks:
            navblock={}
            navblock["title"]=nb.title  #store block title
            #query nvavlinks belongs to this block
            query=m.Navlinks.all()
            navlinks=query.filter("block_name =",nb.name)
            navlinks.order("sequence")
            navlink_list=[]  #for storing navblinks objects
            for nl in navlinks:
                navlink={}
                navlink["title"]=nl.title
                navlink["url"]=nl.url
                navlink["target"]=nl.target
                navlink["hint"]=nl.hint
                navlink_list.append(navlink) #store this link
            navblock["navlinks"]=navlink_list #store block links
            navblock_list.append(navblock)  #store this block
        info["navblocks"]=navblock_list
        url="templates/main.htm"
        path=os.path.join(os.path.dirname(__file__), url)
        content=template.render(path,{'info':info})
        self.response.out.write(content)

config={}
config['webapp2_extras.sessions']={'secret_key':'my-super-secret-key'}
app = webapp2.WSGIApplication([
    ('/', MainHandler),
    ('/login', login),
    ('/check_member', check_member),
    ('/logout', logout),
    ('/hometabs', hometabs),
    ('/home', home),
    ('/change_theme', change_theme),
    ('/list_visitors', list_visitors),
    ('/get_visitors', get_visitors),
    ('/list_members', list_members),
    ('/get_members', get_members),
    ('/remove_visitors', remove_visitors),
    ('/add_member', add_member),
    ('/update_member', update_member),
    ('/remove_member', remove_member),
    ('/list_headerlinks', list_headerlinks),
    ('/get_headerlinks', get_headerlinks),
    ('/add_headerlink', add_headerlink),
    ('/update_headerlink', update_headerlink),
    ('/remove_headerlink', remove_headerlink),
    ('/list_navblocks', list_navblocks),
    ('/get_navblocks', get_navblocks),
    ('/add_navblock', add_navblock),
    ('/update_navblock', update_navblock),
    ('/remove_navblock', remove_navblock),
    ('/list_navlinks', list_navlinks),
    ('/get_navlinks', get_navlinks),
    ('/add_navlink', add_navlink),
    ('/update_navlink', update_navlink),
    ('/remove_navlink', remove_navlink),
    ('/settings', settings),
    ('/update_settings', update_settings)
], debug=True, config=config)

這裡藍色就是有修改的部分, 在 MainHandler 部分因為已利用 check_login() 檢查連線狀態, 因此通過後就直接渲染 main.htm 網頁了.

好了, 以上就是這次為期兩個月玩 GAE 的成果紀錄, 不記下來保證一個月後都忘光光. 檔案全部壓縮封存候用 (下載原始碼(備用下載點). GAE 要暫時 hold 一下, 因為我要開始忙工作日誌了, 希望年底前可以交差.

2016-03-04 補充 :

今天重新審視了一遍程式碼, 又發現了一些 bug 與須改進之處 :

1. 對話框增加取消鈕 :

新增資料對話框增加了 "取消" 按鈕, 按下時會將對話框關閉, 例如新增使用者對話框取消動作處理程式為 :

    $("#cancel_member").bind("click",function(){
      $("#member_dialog").dialog("close");
      });


2. Members 資料表新增 name 欄位, list_members 增加搜尋框.

3. 新增 "使用者設定" 頁籤, 讓使用者可自行改密碼, Email 等.


4. 導覽區塊, 導覽連結, 標頭連結等列表都改為分頁方式顯示.

5. 有搜尋的 Datagrid 重新載入鈕 (reload) 傳送參數 search_field="" :

避免上次搜尋的 search_field 值在 reload 時持續傳出值, 使得無法載入全部資料. 這樣就界定了 datagrid 列表右上角之 reload 為 "全部載入", 而下方分頁工具列上的 reload 為 "現況載入" (之前有搜尋的話會持續).

另外, 我測試了 CDN 供給 Easyui 函式庫 (刪除 jqueryeasyui.htm 這個檔案, 然後將 jqueryeasyui_cdn.htm 複製為 jqueryeasyui.htm), 發現目前已可使用了 (前陣子的 bug 改好了?), 所以可以把 static 目錄下的 easyui 資料夾刪除了, 這樣壓縮後的 zip 只有 33K 而已, 如果自備 Easyui 函式庫的話, 壓縮後就要 814K, 還真是大.

# 更新版下載


沒有留言 :