##language:zh #pragma section-numbers on ||[[KarrigellQuickIn|Karrigell 快速体验文集]]||'''karrigell+storm web快速开发'''|| ::-- ZoomQuiet [<>] <> ## 默许导航,请保留 <> = 装在推车里的暴风雪 = * 原始文章: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操作 * 简单的部署步骤 == 先是一点背景: == === 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 from databases.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.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) 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来决定调用哪个方法 例如下面的表单实现用户登录: {{{
用户名
密码
}}} Include 和 raise HTTP_REDIRECTION 这两种实现url转向的方法类似 java servlet编程中的 sendRedirect 和 forward 通过_checkUserLogin /_checkIsAdmin 提高安全性,实现 rails中的 before filter 的效果。 [ 本帖最后由 lvscar 于 2007-12-17 00:40 编辑 ] === View 层代码 === 用户列表界面`userList.pih:`{{{ project digyn <% Include("/digyn/web/banner.frag") Include("/digyn/web/side.frag.pih") %>
<% pager = userPager %> <%for user in userPager.nowList: %> <%end%>
用户名字现参与项目删除授权为管理员
<%=user.name.encode('utf-8')%> <% for p in user.joinProjects :%> <%=p.name.encode('utf-8')%>  <% end %> <% if user.manageProjects.count() >1 :%> 项目负责中 <% end %> <% else :%> 删除该用户 <% end %> <% else: print "onclick='changeState(this)'" %> <% if (user.is_admin):%> checked <% end %> >
<% print "

共有记录 %s条,分为%s页,每页%s条记录 ,当前第%s页
" % (pager.totleElementNumber,pager.totlePageNum,pager.pageSize,pager.currentPN) %> <% if pager.havePrev():%> <% print "上一页" % (pager.currentPN-1) %> <%end%> <% if pager.haveNext():%> <% print "下一页" % (pager.currentPN+1) %> <%end%>

}}} 分页器:`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)