> 活在这个到处web n.0 的时代,无论工作还是爱好,出个web小项目,这需求对咱程序员实在是太普遍了。 > 作为python爱好者,你会杂办? > 把django资料找出来花个3个月,熟悉它庞大的结构。独特的ORM,和各种taglib ? > 哈,俺们又不是可怜的ruby用户。 > 你完全可以在python世界里信手拈起最适合你的组件,以最快的速度构建起你自己的web框架。
> 如果你的项目符合以下条件,你或许可以考虑 karrigell + storm
> * 不需要承载海量用户并发访问 > * 快速上手的开发过程 > * 需要高可读性和易于维护的代码 > * 延续你在java中习惯的mvc思想 > * 使用人性化的ORM进行db操作 > * 简单的部署步骤
> == 先是一点背景: ==
> === Karrigell: ===
> Karrigell 是轻量级的web框架。灵活,直观,数据库/ORM/模板引擎 独立。
> Karrigell 很容易上手,花个1小时把tutorial一看,就能用html和python代码拼出个简单的web程序出来。
> Karrigell 支持多种方式混合python代码和html。无论你以前有哪一派的背静,都能在karrigell里延续你的经验和习惯。
> Karrigell 部署简单。有python运行环境,就能让它跑的欢畅。
> Karrigell代码成熟。 5年来Karrigell基本保持着半年一个版本速度在成长。
> === 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从一开始就为着同时支持低端小程序和高端(多个数据库,十亿级数据量)而设计。
> === 相关学习资料 === > 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的翻译.
> == 对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那样的形式主义美。
> 下面是,我的一个小项目的 > === 代码安排: === > 在 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 中。
> === 集成Storm: === > karrigell 集成storm可以说是非常方便。 把storm 解压后,放到 karrigell的 /databases 目录。 > 在我们的项目的 conf/ 目录下 建立个storm_conf.py 的module 内容如下: > {{{#!python > fromdatabases.storm.locals import * > db_url = "postgres://lvs:car@localhost/digyn_dev"
> database = None > store = None > def getStore(): > global store > if store == None: > store = Store(getDatabase())
> return store
> def getDatabase(): > global database > if database == None: > database = create_database(db_url) > return database
> #因为采用了module 全局变量。引用此module在最好统一为绝对包名引用
> }}}
> 然后,我们就可以在service里 像这样自然的进行数据的持久化操作: > {{{#!python > from webapps.digyn.model.orm_models import * > from webapps.digyn.util.pager import Pager > from webapps.digyn.conf import storm_conf > store = storm_conf.getStore() > def add(moduleId,title,bugInfo,findDate,findUserId): > bug = Bug(moduleId,title,findDate,findUserId,bugInfo) > bug.bug_state = constantValue.bugState_new > store.add(bug) > store.commit() > return bug.id
> def get(bugId): > return store.get(Bug,bugId)
> def getBugsPagerForModule(moduleId,bugState,pageNumber=1,pageSize=10): > """获取特定模块下特定状态的bug""" > resList = store.find(Bug,Bug.module_id == moduleId,Bug.bug_state > == bugState).order_by(Bug.id) > presList = resList[(pageNumber-1)*pageSize:pageNumber*pageSize] > return Pager(presList,pageNumber,pageSize,resList.count())
> }}}
> [ 本帖最后由 lvscar 于 2007-12-17 01:12 编辑 ]
> === 后台到前台的代码片段 ===
> 从后台到前台实现用户管理功能的代码片段:
> 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.projec-t_id','Project.id') > joinModules = > ReferenceSet(id,'_UserModuleRelation.user_id','_UserModuleRelation.module_i-d','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) > else: > super(User,self).__setattr__(name,value)
> }}}
> 虽说 Storm orm对象不需要继承特别的父类,但继承Storm类会带来一个方便, > 在建立对象关系时,可以用字符串引用其他类。
> Service层代码:`userService.py:` > {{{#!python > import md5 > from webapps.digyn.conf import storm_conf > from common.exception import * > from webapps.digyn.model.orm_models import User > from webapps.digyn.util.pager import Pager
> store = storm_conf.getStore()
> def addUser(name,password): > if checkUserNameUsed(name): > raise NameDuplicate, name > user = User(name,password) > store.add(user) > store.commit()
> def checkUserNameUsed(name): > if store.find(User,User.name == name).one(): > return True > else: > return False
> def deleteUser(userId): > user = get(userId) > store.remove(user) > store.commit()
> def loginValidate(name,password): > password = md5.new(password).digest() > user = store.find(User,User.name == name,User.password == password).one() > if user: > return user > else: > return False > def get(userId): > return store.get(User,int(userId))
> def getAllUser(): > return store.find(User)
> def getManageProjects(userId):
> user = store.get(User,int(userId)) > projectList = [] > for p in user.manageProjects: > projectList.append(p) > return projectList
> def getjoinProjects(userId): > user = store.get(User,int(userId)) > projectList = [] > for p in user.joinProjects: > projectList.append(p) > return projectList > def assignAdmin(act,userId): > user = get(userId) > if act == 'add': > user.is_admin = True > elif act == 'remove': > user.is_admin = False > store.commit()
> def getUserPager(pageNumber =1,nameQueryStr = None,pageSize=10): > if (nameQueryStr and len(nameQueryStr) > 0): > resList = store.find(User,User.name.like(u"%"+nameQueryStr+u"%")) > else: > resList = store.find(User) > resList = resList[(pageNumber-1)*pageSize:pageNumber*pageSize] > 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 编辑 ]
> === Control层代码 === > * `userControl.ks`: > {{{#!python > #-*- coding:utf-8 -*- > from webapps.digyn.service import userService > from common.exception import * > PageSize = 10
> def register(name,password,password_again): > name = unicode(name,'utf-8') > if(password != password_again): > Include("/digyn/web/user/register.pih",flash="两次输入的密码不符") > return > 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')))
> def login(name,password): > name = unicode(name,'utf-8') > user = userService.loginValidate(name,password) > if user: > session = Session() > session.userId = user.id > if user.is_admin: > session.is_admin = True > else: > session.is_admin = False > Include("/digyn/web/user/userIndex.pih",user=user) > return > else: > Include("/digyn/web/user/login.pih" ,flash="该用户不存在或密码错误")
> def logout(): > Session().close() > raise HTTP_REDIRECTION,"/digyn"
> def userManage(act,pageNumber=1): > pageNumber = int(pageNumber) > _checkIsAdmin()
> if act == "list": > _getUserPager(pageNumber)
> def _getUserPager(pageNumber ,nameQueryStr = None,pageSize=PageSize): > userPager = userService.getUserPager(int(pageNumber),pageSize=pageSize) > Include("/digyn/web/admin/userList.pih",userPager=userPager)
> def assignAdmin(act,userId): > """设定用户是否为系统管理员""" > _checkIsAdmin() > userId = int(userId) > if act == "add": > userService.assignAdmin("add",userId) > elif act == "remove": > userService.assignAdmin("remove",userId) > else: > print "erroe" > return > print "success"
> def deleteUser(userId): > _checkIsAdmin() > userId = int(userId) > userName = userService.get(userId).name > userService.deleteUser(userId) > userPager = userService.getUserPager(1,pageSize=pageSize) > Include("/digyn/web/admin/userList.pih",userPager=userPager,flash="用户 > %s 已经删除" %(userName.encode('utf-8')))
> def _checkUserLogin(): > if not hasattr(Session(),'userId'): > Session().close() > Include("/digyn/web/user/login.pih",flash="请先登录") > raise SCRIPT_END > def _checkIsAdmin(): > if(( not hasattr(Session(),'is_admin')) or (not Session().is_admin)): > Session().close() > Include("/digyn/index.pih",flash="你未被授权访问") > 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 编辑 ]
> === 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?userI-d=<%=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:` > {{{#!python > import math > class Pager(object): > def __init__(self,nowList,currentPN,pageSize,totleElementNumber): > self.nowList = nowList > self.currentPN = currentPN > self.pageSize = pageSize > self.totleElementNumber = totleElementNumber
> def getTotlePageNum(self): > return int(math.ceil(self.totleElementNumber / float(self.pageSize))) > totlePageNum = property(fget=getTotlePageNum,doc="return totle page number") > def havePrev(self): > if self.currentPN >1: > return True > else: > return False > def haveNext(self): > if ((self.currentPN*self.pageSize)<self.totleElementNumber): > return True > else: > return False
> }}}
> [ 本帖最后由 lvscar 于 2007-12-17 00:46 编辑 ]
> = 反馈 = > [[PageComment2]]
> -- > '''Time is unimportant, only life important! > 过程改进乃是开始催生可促生靠谱的人的组织! > '''http://zoomquiet.org > 博 @http://blog.zoomquiet.org/pyblosxom/ > 维 @http://wiki.woodpecker.org.cn/moin/ZoomQuiet > 豆 @http://www.douban.com/people/zoomq/ > 看 @http://zoomq.haokanbu.com/ > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > Pls. usage OOo to replace M$ Office.http://zh.openoffice.org > Pls. usage 7-zip to replace WinRAR/WinZip. http://7-zip.org > You can get the truely Freedom 4 software.