過去兩個月嘗試把一年前用 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, 還真是大.
# 更新版下載
沒有留言 :
張貼留言