熟悉Pyramid之后,可以很方便的建立一个简单的CRUD程序。下面我们展示一下一个最简单的书籍管理应用的构建过程。
一、建立项目框架
pcreate -s alchemy simpleCRUD
cd simpleCRUD
python setup.py develop
二、建立model
model.py 加入代码:
class Book(Base):
__tablename__ = 'book'
id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
name = Column(Unicode(64), unique=True, nullable=False)
author = Column(Unicode(32), nullable=True)
desc = Column(Unicode, nullable=True)
ISBN = Column(Unicode(20), nullable=True)
price = Column(Float, nullable=True)
def __init__(self, name, author, desc, ISBN, price):
self.author = author
self.desc = desc
self.ISBN = ISBN
self.price = price
initializedb.py 中main函数变更为:
def main(argv=sys.argv):
if len(argv) != 2:
usage(argv)
config_uri = argv[1]
setup_logging(config_uri)
settings = get_appsettings(config_uri)
engine = engine_from_config(settings, 'sqlalchemy.')
DBSession.configure(bind=engine)
Base.metadata.create_all(engine)
with transaction.manager:
book = Book(name='Python', author='chen', desc='desc', ISBN='978-7-121-06874-4', price=69.80)
DBSession.add(book)
然后运行
initialize_simpleCRUD_db development.ini
建立数据库,并在数据库中插入第一条记录。
三、建立route结构
在__init__.py中的main函数加入代码:
config.add_route('book_list', '/book/list')
config.add_route('book_detail', '/book/detail/{id}')
config.add_route('book_add', '/book/add')
config.add_route('book_edit', '/book/edit/{id}')
config.add_route('book_delete', '/book/delete/{id}')
通过上述URL结构即可构造一个简单的CRUD应用。
四、第一个功能list
首先实现list功能,确保list能正确读取数据库数据。因此,构建单元测试:
def test_book_list(self):
from .views import book_list
request = testing.DummyRequest()
info = book_list(request)
self.assertEqual(len(info['books']), 1)
self.assertEqual(info['books'][0].name, 'Python')
为了满足这个单元测试运行的条件,修改setUp函数为:
def setUp(self):
self.config = testing.setUp()
from sqlalchemy import create_engine
engine = create_engine('sqlite://')
from .models import (
Base,
Book,
)
DBSession.configure(bind=engine)
Base.metadata.create_all(engine)
with transaction.manager:
book = Book(name='Python', author='chen', desc='desc', ISBN='978-7-121-06874-4', price=69.80)
DBSession.add(book)
该setUp函数建立一个专门用于单元测试的内存数据库,然后建立所有model定义的表结构,并插入第一条记录。在完成上面的初始化之后,我们的第一个单元测试的逻辑是正确的了。
现在再来构建对应的view:
@view_config(route_name='home', renderer='simplecrud:templates/book/book_list.pt')
@view_config(route_name='book_list', renderer='simplecrud:templates/book/book_list.pt')
def book_list(request):
books = DBSession.query(Book).all()
return dict(books = books)
这里就不分页了,全部一页取出显示。并且首页就是list页。
这时候运行 python setup.py test 即可运行单元测试查看是否满足。
但单元测试满足之后,现在使用pserver development.ini还是无法看到页面内容的,这需要建立对应的渲染模版,我们这里先使用chameleon模版来完成这个工作。
该模版的主要工作就是显示一个table,里面包含所有数据记录。在模版中的tal属性是chameleon定义的特殊属性。
1. tal:repeat="item books" 循环books列表,逐项取出赋值给item
2. repeat.item.number 循环计数器
3. tal:content="repeat.item.number" 替换该标签的内容部分,这里就是内容“1”
4. request.route_path('book_detail', id=item.id) 生成book_detail链接
5. tal:attributes="href string:${request.route_path('book_edit', id=item.id)}" 将标签的href属性替换
五、显示详细信息页面
详细信息页面基本构成跟list页面基本类似,最主要的区别是,当book id不存在时,详细信息页面需要返回HTTPForbidden,我们需要在单元测试中覆盖这种情况。
def test_book_detail_unauthorized(self):
from pyramid.httpexceptions import HTTPForbidden
from .views import book_detail
request = testing.DummyRequest()
request.matchdict['id'] = 555
info = book_detail(request)
self.assertIsInstance(info, HTTPForbidden)
注意,如果我们想要在视图中使用raise的化,要使用self.assertRaises(HTTPForbidden, view_fn, request)这种方式来捕获。
六、使用simpleform来添加记录页面
添加记录程序一般会涉及到显示Form、存储、出错这样三种情况,因此,构建三个测试用例:
在request中什么都没有的情况下,显示form
def test_book_add_form(self):
from pyramid_simpleform.renderers import FormRenderer
from .views import book_add
request = testing.DummyRequest()
info = book_add(request)
self.assertIsInstance(info['renderer'], FormRenderer)
在request中指定了是POST时,将取到的数据存入数据库,成功后页面转向list页面,最后验证存入正确
def test_book_add_save(self):
from pyramid.httpexceptions import HTTPFound
from .views import book_add
from .models import Book
_registerRoutes(self.config)
request = testing.DummyRequest({'name':'a new book',
'author':'a new author',
'ISBN':'a new ISBN',
'desc':'a new desc',
'price':'6.0',
'submit':'save'})
request.method = 'POST'
info = book_add(request)
self.assertIsInstance(info, HTTPFound)
added = DBSession.query(Book).filter(Book.id==2).first()
self.assertEqual(added.name, 'a new book')
self.assertEqual(added.author, 'a new author')
在存入数据有错误的情况下,返回提交界面,并确保不会变更数据库已有数据
def test_book_add_conflict(self):
from pyramid_simpleform.renderers import FormRenderer
from .views import book_add
from .models import Book
_registerRoutes(self.config)
request = testing.DummyRequest({'name':'Python',
'author':'a new author',
'ISBN':'a new ISBN',
'desc':'a new desc',
'price':'6.0',
'submit':'save'})
request.method = 'POST'
info = book_add(request)
self.assertIsInstance(info['renderer'], FormRenderer)
added = DBSession.query(Book).filter(Book.id==1).first()
self.assertEqual(added.name, 'Python')
self.assertEqual(added.author, 'chen')
self.assertEqual(added.ISBN, '978-7-121-06874-4')
下面开始构造视图方法。一般情况下,从form读取数据都有有个验证、校验、转换过程,为了程序简单,我们采用simpleform,以及formencode用于数据校验,form构成。
class BookSchema(Schema):
filter_extra_fields = True # 过滤掉其他字段
allow_extra_fields = True # 允许form中有其他字段
name = validators.String(min=2, max=64, not_empty=True) # name最小2字符,最大64
author = validators.String(max=32)
desc = validators.String()
ISBN = validators.String(max=20)
price = validators.Number(max=1000) # price为数字,浮点型
@view_config(route_name='book_add', renderer='simplecrud:templates/book/book_add.pt')
def book_add(request):
form = Form(request, schema=BookSchema)
if form.validate(): # 如果是正确提交
book = Book(form.data.get("name"),
form.data.get("author"),
form.data.get("desc"),
form.data.get("ISBN"),
form.data.get("price"))
try:
DBSession.add(book)
DBSession.flush()
transaction.commit()
return HTTPFound(location=route_path("book_list", request)) # 返回到list页面
except IntegrityError: # name重复,设置form出错信息
transaction.abort()
form.errors["global_error"] = 'database insert error, maybe book name conflict.'
except Exception, e:
transaction.abort()
form.errors["global_error"] = 'database error.' + str(e)
log.error("database error!")
return dict(renderer=FormRenderer(form)) 返回form页面
这里需要注意的是,单元测试中,并没有去构造route表,但我们程序的重定向中使用了route_path方法,因此需要在单元测试中手工构造route表:
def _registerRoutes(config):
config.add_route('book_list', '/book/list')
config.add_route('book_detail', '/book/detail/{id}')
config.add_route('book_add', '/book/add')
config.add_route('book_edit', '/book/edit/{id}')
config.add_route('book_delete', '/book/delete/{id}')
七、edit,delete
edit、delete基本类似new与detail,值得注意的是,edit时尽可能只访问一次数据库、减轻数据库访问量。
八、完整程序