karrigell+storm web快速开发 |
::-- ZoomQuiet [2007-12-17 00:41:37]
Contents
1. 装在推车里的暴风雪
原始文章:http://bbs.chinaunix.net/thread-1030014-1-1.html 2007-12-16 23:02
活在这个到处web n.0 的时代,无论工作还是爱好,出个web小项目,这需求对咱程序员实在是太普遍了。 作为python爱好者,你会杂办? 把django资料找出来花个3个月,熟悉它庞大的结构。独特的ORM,和各种taglib ? 哈,俺们又不是可怜的ruby用户。 你完全可以在python世界里信手拈起最适合你的组件,以最快的速度构建起你自己的web框架。
如果你的项目符合以下条件,你或许可以考虑 karrigell + storm
- 不需要承载海量用户并发访问
- 快速上手的开发过程
- 需要高可读性和易于维护的代码
- 延续你在java中习惯的mvc思想
- 使用人性化的ORM进行db操作
- 简单的部署步骤
1.1. 先是一点背景:
1.1.1. Karrigell:
Karrigell 是轻量级的web框架。灵活,直观,数据库/ORM/模板引擎 独立。
Karrigell 很容易上手,花个1小时把tutorial一看,就能用html和python代码拼出个简单的web程序出来。
Karrigell 支持多种方式混合python代码和html。无论你以前有哪一派的背静,都能在karrigell里延续你的经验和习惯。
Karrigell 部署简单。有python运行环境,就能让它跑的欢畅。
Karrigell代码成熟。 5年来Karrigell基本保持着半年一个版本速度在成长。
1.1.2. Storm
Storm 是由Canonical开发的一套 Python ORM库,用在支持着ubunut的Launchpad项目上。
以下是Storm的一些亮点(翻译自storm官方网站):
干净的轻量级API使得Storm的学习上手过程相对轻松。基于Storm的代码也有着友好的维护性.
Storm由测试驱动模式开发,任何一行没经过测试的代码都被认为存在bug。
Storm 中的Model类 不需要特别的构造器,也不需要强制使用专门的基类。
Storm 整体设计得很好。 (代码中不同的部分有着非常清晰的边界。公共api数量小和意义明确)
Storm从一开始以同时支持轻量级(SQLite)和重量级(PostgreSQL / MYSQL)而设计。
Storm代码遵循KISS原则编写,代码简单易读,调试方便。
Storm从一开始就为着同时支持低端小程序和高端(多个数据库,十亿级数据量)而设计。
1.1.3. 相关学习资料
karrigell和storm 都很pythonic,看完toturial 基本就能上手. 想深入了解就直接看代码。代码均干净整洁,注释详尽。看这类项目的码是享受啊:
karrigell的toturial地址:http://karrigell.sourceforge.net/en/front.htm
ChumpKlutz 朽木兄对这这份toturial进行了翻,可以到他的blog 查看:http://blog.csdn.net/chumpklutz/
storm的toturial地址:https://storm.canonical.com/Tutorial
- 目前还没有中文版,俺争取下月完成这份toturial的翻译.
1.2. 对Karrigell的MVC化规范
karrigell 附带的demo是学习karrigell最好的途径。Karrigell的各种用法都在这七八个demo中有精彩的体现。但或许是作者刻意想通过这几个 demo 体现出karrigell的灵活,phi,hip,ks 混合起来蛮容易把人搞晕。而且附带的几个demo 代码组织都很松散,往往就是把一堆phi,hip,ks,js,gif放在一起了事。如果之前看过rails或django 可能会不太适应karrigell demo中的这种凌乱感。
哈,别慌,如果愿意,你完全可以把你的项目按照天条似的mvc结构来组织。加上少许规范,karrigell也能秀出rails那样的形式主义美。
下面是,我的一个小项目的
1.2.1. 代码安排:
在 karrigell webapps 目录下,用你喜欢的名字命名你的karrigell工程。 工程目录下,新建:
conf/ control/ model/ service/ test/ util/ web/ 这几个目录, 以及 index.pih 和 __init__.py
我的习惯是把目录恒量,和数据库配置放在 conf 下。
- ORM 对象 放在 model 目录;
- service目录下放置业务代码;
- util里放入第三方库;
- test里写点小测试代码。
- 所有前台代码,都放到web目录里,所以web目录下可以再新建几个js/ css/ uploadFiles/ 这样的目录。
- web目录下,只写和前台展现相关的代码,根据页面的复杂程度,在pih和hip中选择。
- contorl目录下,只写页面跳转,为web目录下的pih和hip提供变量 和调用相关业务方法的代码。 ks是最好的选择。 同一个对象,不同的web行为。可以写在成一个ks中的多个方法。
- tips
- ks 中不能直接捕获引入方法抛出的异常,因为异常在 core.k_script 里已经被捕获并做了处理。
我的解决方法是,把所有自己用到的异常的自定义父类 添加到 core/k_script.py 154行左右 直接抛出捕获异常的 except 字句参数里。 然后再把所有用到的异常引入 modules/mod_ks.py 中。
1.2.2. 集成Storm:
karrigell 集成storm可以说是非常方便。 把storm 解压后,放到 karrigell的 /databases 目录。 在我们的项目的 conf/ 目录下 建立个storm_conf.py 的module 内容如下:
1 from databases.storm.locals import *
2 db_url = "postgres://lvs:car@localhost/digyn_dev"
3
4 database = None
5 store = None
6 def getStore():
7 global store
8 if store == None:
9 store = Store(getDatabase())
10
11 return store
12
13
14 def getDatabase():
15 global database
16 if database == None:
17 database = create_database(db_url)
18 return database
19
20 #因为采用了module 全局变量。引用此module在最好统一为绝对包名引用
然后,我们就可以在service里 像这样自然的进行数据的持久化操作:
1 from webapps.digyn.model.orm_models import *
2 from webapps.digyn.util.pager import Pager
3 from webapps.digyn.conf import storm_conf
4 store = storm_conf.getStore()
5 def add(moduleId,title,bugInfo,findDate,findUserId):
6 bug = Bug(moduleId,title,findDate,findUserId,bugInfo)
7 bug.bug_state = constantValue.bugState_new
8 store.add(bug)
9 store.commit()
10 return bug.id
11
12 def get(bugId):
13 return store.get(Bug,bugId)
14
15 def getBugsPagerForModule(moduleId,bugState,pageNumber=1,pageSize=10):
16 """获取特定模块下特定状态的bug"""
17 resList = store.find(Bug,Bug.module_id == moduleId,Bug.bug_state == bugState).order_by(Bug.id)
18 presList = resList[(pageNumber-1)*pageSize:pageNumber*pageSize]
19 return Pager(presList,pageNumber,pageSize,resList.count())
[ 本帖最后由 lvscar 于 2007-12-17 01:12 编辑 ]
1.2.3. 后台到前台的代码片段
从后台到前台实现用户管理功能的代码片段:
storm ORM 对象 orm_models.py: {{{!python import md5 from databases.storm.locals import *
class User(Storm):
storm_table='users' id = Int(primary=True) password = RawStr() name = Unicode() info = Unicode() is_admin = Bool() manageProjects = ReferenceSet('User.id','Project.manager_id') joinProjects = ReferenceSet(id,'_UserProjectRelation.user_id','_UserProjectRelation.project_id','Project.id') joinModules = ReferenceSet(id,'_UserModuleRelation.user_id','_UserModuleRelation.module_id','Module.id')
def init(self,name,password,is_admin=False):
- self.name = name self.password = password self.is_admin = is_admin
def setattr(self,name,value):
- if name == 'password':
#self.dict['password'] = value #设值是通过Storm 属性类的set方法(重载 '='操作符 )实现的。改变instance中的值,变化不会被storm察觉 processedPassword = md5.new(value).digest() User.password.set(self,processedPassword)
super(User,self).setattr(name,value)
}}}
虽说 Storm orm对象不需要继承特别的父类,但继承Storm类会带来一个方便, 在建立对象关系时,可以用字符串引用其他类。
Service层代码:userService.py:
1 import md5
2 from webapps.digyn.conf import storm_conf
3 from common.exception import *
4 from webapps.digyn.model.orm_models import User
5 from webapps.digyn.util.pager import Pager
6
7 store = storm_conf.getStore()
8
9 def addUser(name,password):
10 if checkUserNameUsed(name):
11 raise NameDuplicate, name
12 user = User(name,password)
13 store.add(user)
14 store.commit()
15
16 def checkUserNameUsed(name):
17 if store.find(User,User.name == name).one():
18 return True
19 else:
20 return False
21
22 def deleteUser(userId):
23 user = get(userId)
24 store.remove(user)
25 store.commit()
26
27 def loginValidate(name,password):
28 password = md5.new(password).digest()
29 user = store.find(User,User.name == name,User.password == password).one()
30 if user:
31 return user
32 else:
33 return False
34 def get(userId):
35 return store.get(User,int(userId))
36
37 def getAllUser():
38 return store.find(User)
39
40 def getManageProjects(userId):
41
42 user = store.get(User,int(userId))
43 projectList = []
44 for p in user.manageProjects:
45 projectList.append(p)
46 return projectList
47
48 def getjoinProjects(userId):
49 user = store.get(User,int(userId))
50 projectList = []
51 for p in user.joinProjects:
52 projectList.append(p)
53 return projectList
54 def assignAdmin(act,userId):
55 user = get(userId)
56 if act == 'add':
57 user.is_admin = True
58 elif act == 'remove':
59 user.is_admin = False
60 store.commit()
61
62 def getUserPager(pageNumber =1,nameQueryStr = None,pageSize=10):
63 if (nameQueryStr and len(nameQueryStr) > 0):
64 resList = store.find(User,User.name.like(u"%"+nameQueryStr+u"%"))
65 else:
66 resList = store.find(User)
67 resList = resList[(pageNumber-1)*pageSize:pageNumber*pageSize]
68 return Pager(resList,pageNumber,pageSize,resList.count())
顶楼那个tips的意义,就在于可以让Service层中写addUser方法时,我们可以直接抛出一个自定意异常,让control层代码可以写成下面这种格式:
try: userService.addUser(name,password) except NameDuplicate,userName: Include("/digyn/web/user/register.pih",flash="用户 %s 已经存在" %str(userName)) return Include("/digyn/index.pih",flash="%s 你的帐号已添加,请登录" % (name.encode('utf-8')))
[ 本帖最后由 lvscar 于 2007-12-17 00:25 编辑 ]
1.2.4. Control层代码
userControl.ks:
1 #-*- coding:utf-8 -*-
2 from webapps.digyn.service import userService
3 from common.exception import *
4 PageSize = 10
5
6 def register(name,password,password_again):
7 name = unicode(name,'utf-8')
8 if(password != password_again):
9 Include("/digyn/web/user/register.pih",flash="两次输入的密码不符")
10 return
11 try:
12 userService.addUser(name,password)
13 except NameDuplicate,userName:
14 Include("/digyn/web/user/register.pih",flash="用户 %s 已经存在" %str(userName))
15 return
16 Include("/digyn/index.pih",flash="%s 你的帐号已添加,请登录" % (name.encode('utf-8')))
17
18 def login(name,password):
19 name = unicode(name,'utf-8')
20 user = userService.loginValidate(name,password)
21 if user:
22 session = Session()
23 session.userId = user.id
24 if user.is_admin:
25 session.is_admin = True
26 else:
27 session.is_admin = False
28 Include("/digyn/web/user/userIndex.pih",user=user)
29 return
30 else:
31 Include("/digyn/web/user/login.pih" ,flash="该用户不存在或密码错误")
32
33 def logout():
34 Session().close()
35 raise HTTP_REDIRECTION,"/digyn"
36
37 def userManage(act,pageNumber=1):
38 pageNumber = int(pageNumber)
39 _checkIsAdmin()
40
41 if act == "list":
42 _getUserPager(pageNumber)
43
44 def _getUserPager(pageNumber ,nameQueryStr = None,pageSize=PageSize):
45 userPager = userService.getUserPager(int(pageNumber),pageSize=pageSize)
46 Include("/digyn/web/admin/userList.pih",userPager=userPager)
47
48 def assignAdmin(act,userId):
49 """设定用户是否为系统管理员"""
50 _checkIsAdmin()
51 userId = int(userId)
52 if act == "add":
53 userService.assignAdmin("add",userId)
54 elif act == "remove":
55 userService.assignAdmin("remove",userId)
56 else:
57 print "erroe"
58 return
59 print "success"
60
61 def deleteUser(userId):
62 _checkIsAdmin()
63 userId = int(userId)
64 userName = userService.get(userId).name
65 userService.deleteUser(userId)
66 userPager = userService.getUserPager(1,pageSize=pageSize)
67 Include("/digyn/web/admin/userList.pih",userPager=userPager,flash="用户 %s 已经删除" %(userName.encode('utf-8')))
68
69 def _checkUserLogin():
70 if not hasattr(Session(),'userId'):
71 Session().close()
72 Include("/digyn/web/user/login.pih",flash="请先登录")
73 raise SCRIPT_END
74 def _checkIsAdmin():
75 if(( not hasattr(Session(),'is_admin')) or (not Session().is_admin)):
76 Session().close()
77 Include("/digyn/index.pih",flash="你未被授权访问")
78 raise SCRIPT_END
Control层一个方法对应 一个web动作,表单参数名直接用做方法参数。 通过url来决定调用哪个方法 例如下面的表单实现用户登录:
<form action="/digyn/control/userControl.ks/login" method='post'> <input name="name">用户名</input><br/> <input type="password" name="password">密码</input><br/> <input type="submit" value="登录"> </form>
Include 和 raise HTTP_REDIRECTION 这两种实现url转向的方法类似 java servlet编程中的 sendRedirect 和 forward
通过_checkUserLogin /_checkIsAdmin 提高安全性,实现 rails中的 before filter 的效果。
[ 本帖最后由 lvscar 于 2007-12-17 00:40 编辑 ]
1.2.5. View 层代码
用户列表界面userList.pih:
<html> <head> <title>project digyn</title> <META http-equiv=Content-Type content="text/html; charset=utf-8"> <link href="/digyn/web/global.css" media="all" rel="Stylesheet" type="text/css" /> <script type="text/javascript" src="/digyn/web/js/prototype.js"></script> <script type="text/javascript"> function changeState(checkBoxObj){ var userId = checkBoxObj.value; var act = ""; if (checkBoxObj.checked == true){ act = "add"; }else{ act = "remove"; } new Ajax.Request("/digyn/control/userControl.ks/assignAdmin",{ asynchronous: false, method: 'post', parameters: "act="+act+"&userId="+userId, onFailure: function(request){ alert(request.responseText); } }); } </script> </head> <body> <% Include("/digyn/web/banner.frag") Include("/digyn/web/side.frag.pih") %> <div id="main"> <% pager = userPager %> <table> <tr> <td>用户名字</td><td>现参与项目</td><td>删除</td><td>授权为管理员</td> </tr> <%for user in userPager.nowList: %> <tr> <td><%=user.name.encode('utf-8')%></td> <td> <% for p in user.joinProjects :%> <%=p.name.encode('utf-8')%> <% end %> </td> <script type="text/javascript"> function openUrlWithConfirm(url){ if (confirm("确实要删除吗?")){ document.location = url; } } </script> <td> <% if user.manageProjects.count() >1 :%> 项目负责中 <% end %> <% else :%> <a href="#" onclick="openUrlWithConfirm('/digyn/control/userControl.ks/deleteUser?userId=<%=user.id%>')">删除该用户</a> <% end %> </td> <td> <input type="checkbox" value="<%=user.id%>" <% if user.id == Session().userId: print "disabled" %> <% else: print "onclick='changeState(this)'" %> <% if (user.is_admin):%> checked <% end %> > </td> </tr> <%end%> </table> <% print "<p> 共有记录 %s条,分为%s页,每页%s条记录 ,当前第%s页<br/>" % (pager.totleElementNumber,pager.totlePageNum,pager.pageSize,pager.currentPN) %> <% if pager.havePrev():%> <% print "<a href='/digyn/control/userControl.ks/userManage?act=list&pageNumber=%s'>上一页</a>" % (pager.currentPN-1) %> <%end%> <% if pager.haveNext():%> <% print "<a href='/digyn/control/userControl.ks/userManage?act=list&pageNumber=%s'>下一页</a>" % (pager.currentPN+1) %> <%end%> </div> </body> </html>
分页器:pager.py:
1 import math
2 class Pager(object):
3 def __init__(self,nowList,currentPN,pageSize,totleElementNumber):
4 self.nowList = nowList
5 self.currentPN = currentPN
6 self.pageSize = pageSize
7 self.totleElementNumber = totleElementNumber
8
9 def getTotlePageNum(self):
10 return int(math.ceil(self.totleElementNumber / float(self.pageSize)))
11 totlePageNum = property(fget=getTotlePageNum,doc="return totle page number")
12 def havePrev(self):
13 if self.currentPN >1:
14 return True
15 else:
16 return False
17 def haveNext(self):
18 if ((self.currentPN*self.pageSize)<self.totleElementNumber):
19 return True
20 else:
21 return False
[ 本帖最后由 lvscar 于 2007-12-17 00:46 编辑 ]