装在推车里的暴风雪 karrigell+storm web快速开发

装在推车里的暴风雪 karrigell+storm web快速开发

活在这个到处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 内容如下:
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里 像这样自然的进行数据的持久化操作:
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())


从后台到前台实现用户管理功能的代码片段:

storm ORM 对象  
orm_models.py:
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:
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')))

Control层代码
userControl.ks:
#-*- 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 的效果。

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:

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


好帖,顶起来;
我还没看懂,先支持一下!