今天下班後又跑去游泳, 因為明天晚上要去菁菁的班親會沒空去游. 先回家收拾泳衣再騎小摺去, 這樣剛好可以暖身. 這次放聰明了, 下水前沖澡時改沖稍冷的溫水, 不再傻傻地沖熱水. 今天來回游了 14 趟, 再去用水柱沖會痠痛的後腰部, 感覺非常舒服.
很奇怪, 雖然游泳耗費體力, 但游泳回來都精神特好, 不覺得累, 不過還是早點睡為好, 明天可不是假日哩!
2016-03-06 補充 :
今天在班親會上第一次見到菁菁的班導怡如老師, 是個具有濃厚文學底蘊的人, 已經出版好幾本文學作品囉! 我很羨慕作家, 因為走過人生, 作家是那種能在世界上留下痕跡的人. 所謂立德立功立言的三達德, 作家至少能夠立言啊!
2016年3月3日 星期四
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, 還真是大.
# 更新版下載
去年底休假時無意中看到以前寫的 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, 還真是大.
# 更新版下載
2016年3月1日 星期二
2016 年第 9 周記事
週六跑去游泳初體驗, 一口氣游了十趟 25 公尺, 中午又沒午睡, 到了晚上也不覺得睏, 結果週日早上竟然睡到八點過才起床, 可見游泳挺耗體力, 但白天似乎感覺不太出來.
週日 2/28 午睡起來後想去國十下來不遠處的一家冰果室吃吃看 "炸香蕉", 每次經過那裏等紅燈都很納悶那是啥? 我提過之後, 菁菁就吵著要去, 我想就順便載爸出去兜兜風. 我們先去岳父的農園看看, 再順路去這家國十下來往美濃方向的 "蕉你來" 冰果室. 車放在巷子進去的停車場再走出來, 老闆是個中年歐吉桑, 一個人既要弄冰品, 又要忙炸物, 看來很忙碌. 我們點了一份炸香蕉, 兩份香蕉清冰 (其實就是三球的紅豆清冰啦), 一份招牌清冰 (一球清冰+火龍果冰淇淋+梅子冰淇淋), 火龍果冰淇淋滋味很特殊, 以前沒吃過, 而炸香蕉就是將將香蕉切成條狀後去油炸, 再撒上梅子粉, 吃起來味道像薯條, 不講的話應該猜不出來那是香蕉哩!
吃完冰回家的路上, 過 7-11 後看到路旁有一個風車, 我好奇它是怎麼做的, 就停車研究一番, 原來是將腳踏車輪框加以改裝焊接而成, 如果裝上個小發電機會更好 :
周一早上跟老張一家去爬旗靈縱走, 從靈山雷音寺上山, 走到距離旗尾山祠還有兩座峰就折返 (估計應該到達了金字面山), 因為老張說要走完全程恐怕要到下午.
這段山路大都在稜線上, 一邊是美濃, 另外一邊是杉林, 底下就是楠梓仙溪, 也不難爬, 要找個時間來走完全程 (可以用去糖廠吃冰引誘菁菁). 只是自己開車去是個問題, 因為不管起點是旗尾山或靈山, 走完後得沿著山下的步道走回起點 (5~6 km), 慢慢走當散步欣賞村落風光當然好, 但腳力差的就會哇哇叫. 有一個辦法是開兩台車分別從靈山與旗尾山登山, 兩路人馬在中間會師交換車鑰匙, 下山時再開另一方的車會合.
# 旗靈縱走(旗尾山縱走靈山頂福德祠)
上週五午飯後跟同事步行到中正路的農友社買了六顆樹苗 : 兩棵紅肉, 一棵紅心, 一棵四季檸檬, 一棵蜜棗, 因為同事是會員, 且二月壽星有 85 折優惠, 六棵原價 899, 打折下來 760 元. 但買回來才覺得似乎買太多了, 應該先買三棵就好, 因為我還得等菜園右側的甘蔗與三株美人蕉清空才行.
週日 2/29 傍晚, 爸先動手砍除三株美人蕉, 去年才從岳父農園移回來種了一年就增生了好多側芽, 大約是 1 變 4~5, 可惜這種香蕉很小, 每條約拇指大, 根本連一口蕉都談不上, 充其量是半口蕉而已. 而且樹身非常高, 葉子也碩大, 嚴重遮住旁邊的兩棵芒果樹的陽光. 既然產量少又遮陰, 乾脆砍掉改種芭樂好了. 種是很容易, 要砍掉卻費了一番功夫. 砍軀幹是不難, 問題在於根, 爸說要把底下的根都挖掉才不會再長出來, 得用山剷才有辦法, 鋤頭較費力. 我花了半小時才把這三株美人蕉的根部徹底挖除.
大咪自除夕前離家後至今未歸, 已逾 23 天了, 這隻老貓不知跑到哪裡去.
週日 2/28 午睡起來後想去國十下來不遠處的一家冰果室吃吃看 "炸香蕉", 每次經過那裏等紅燈都很納悶那是啥? 我提過之後, 菁菁就吵著要去, 我想就順便載爸出去兜兜風. 我們先去岳父的農園看看, 再順路去這家國十下來往美濃方向的 "蕉你來" 冰果室. 車放在巷子進去的停車場再走出來, 老闆是個中年歐吉桑, 一個人既要弄冰品, 又要忙炸物, 看來很忙碌. 我們點了一份炸香蕉, 兩份香蕉清冰 (其實就是三球的紅豆清冰啦), 一份招牌清冰 (一球清冰+火龍果冰淇淋+梅子冰淇淋), 火龍果冰淇淋滋味很特殊, 以前沒吃過, 而炸香蕉就是將將香蕉切成條狀後去油炸, 再撒上梅子粉, 吃起來味道像薯條, 不講的話應該猜不出來那是香蕉哩!
吃完冰回家的路上, 過 7-11 後看到路旁有一個風車, 我好奇它是怎麼做的, 就停車研究一番, 原來是將腳踏車輪框加以改裝焊接而成, 如果裝上個小發電機會更好 :
周一早上跟老張一家去爬旗靈縱走, 從靈山雷音寺上山, 走到距離旗尾山祠還有兩座峰就折返 (估計應該到達了金字面山), 因為老張說要走完全程恐怕要到下午.
遙望旗尾山祠後折返
死亡大峭壁攀繩而下
人頭山頂的三等三角點 (392m)
這段山路大都在稜線上, 一邊是美濃, 另外一邊是杉林, 底下就是楠梓仙溪, 也不難爬, 要找個時間來走完全程 (可以用去糖廠吃冰引誘菁菁). 只是自己開車去是個問題, 因為不管起點是旗尾山或靈山, 走完後得沿著山下的步道走回起點 (5~6 km), 慢慢走當散步欣賞村落風光當然好, 但腳力差的就會哇哇叫. 有一個辦法是開兩台車分別從靈山與旗尾山登山, 兩路人馬在中間會師交換車鑰匙, 下山時再開另一方的車會合.
# 旗靈縱走(旗尾山縱走靈山頂福德祠)
上週五午飯後跟同事步行到中正路的農友社買了六顆樹苗 : 兩棵紅肉, 一棵紅心, 一棵四季檸檬, 一棵蜜棗, 因為同事是會員, 且二月壽星有 85 折優惠, 六棵原價 899, 打折下來 760 元. 但買回來才覺得似乎買太多了, 應該先買三棵就好, 因為我還得等菜園右側的甘蔗與三株美人蕉清空才行.
週日 2/29 傍晚, 爸先動手砍除三株美人蕉, 去年才從岳父農園移回來種了一年就增生了好多側芽, 大約是 1 變 4~5, 可惜這種香蕉很小, 每條約拇指大, 根本連一口蕉都談不上, 充其量是半口蕉而已. 而且樹身非常高, 葉子也碩大, 嚴重遮住旁邊的兩棵芒果樹的陽光. 既然產量少又遮陰, 乾脆砍掉改種芭樂好了. 種是很容易, 要砍掉卻費了一番功夫. 砍軀幹是不難, 問題在於根, 爸說要把底下的根都挖掉才不會再長出來, 得用山剷才有辦法, 鋤頭較費力. 我花了半小時才把這三株美人蕉的根部徹底挖除.
大咪自除夕前離家後至今未歸, 已逾 23 天了, 這隻老貓不知跑到哪裡去.
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))
首先是去 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) :
訂閱:
文章
(
Atom
)
















