Defining the Domain Model¶
The first change we’ll make to our bone-stock paster
-generated
application will be to define a number of resource constructors.
Remember that, because we’re using ZODB to represent our
resource tree, each of these resource constructors represents a
domain model object, so we’ll call these constructors “model
constructors”. For this application, which will be a Wiki, we will need two
kinds of model constructors: a “Wiki” model constructor, and a “Page” model
constructor. Both our Page and Wiki constructors will be class objects. A
single instance of the “Wiki” class will serve as a container for “Page”
objects, which will be instances of the “Page” class.
The source code for this tutorial stage can be browsed via http://github.com/Pylons/pyramid/tree/1.0-branch/docs/tutorials/wiki/src/models/.
Deleting the Database¶
In a subsequent step, we’re going to remove the MyModel
Python model
class from our models.py
file. Since this class is referred to within
our persistent storage (represented on disk as a file named Data.fs
),
we’ll have strange things happen the next time we want to visit the
application in a browser. Remove the Data.fs
from the tutorial
directory before proceeding any further. It’s always fine to do this as long
as you don’t care about the content of the database; the database itself will
be recreated as necessary.
Adding Model Classes¶
The next thing we want to do is remove the MyModel
class from the
generated models.py
file. The MyModel
class is only a sample and
we’re not going to use it.
Note
There is nothing automagically special about the filename models.py
. A
project may have many models throughout its codebase in arbitrarily-named
files. Files implementing models often have model
in their filenames,
or they may live in a Python subpackage of your application package named
models
, but this is only by convention.
Then, we’ll add a Wiki
class. Because this is a ZODB application, this
class should inherit from persistent.mapping.PersistentMapping
. We
want it to inherit from the persistent.mapping.PersistentMapping
class because our Wiki class will be a mapping of wiki page names to Page
objects. The persistent.mapping.PersistentMapping
class provides
our class with mapping behavior, and makes sure that our Wiki page is stored
as a “first-class” persistent object in our ZODB database.
Our Wiki
class should also have a __name__
attribute set to None
at class scope, and should have a __parent__
attribute set to None
at
class scope as well. If a model has a __parent__
attribute of None
in a traversal-based Pyramid application, it means that it’s the
root model. The __name__
of the root model is also always
None
.
Then we’ll add a Page
class. This class should inherit from the
persistent.Persistent
class. We’ll also give it an __init__
method that accepts a single parameter named data
. This parameter will
contain the ReStructuredText body representing the wiki page content.
Note that Page
objects don’t have an initial __name__
or
__parent__
attribute. All objects in a traversal graph must have a
__name__
and a __parent__
attribute. We don’t specify these here
because both __name__
and __parent__
will be set by by a view
function when a Page is added to our Wiki mapping.
As a last step, we want to change the appmaker
function in our
models.py
file so that the root resource of our
application is a Wiki instance. We’ll also slot a single page object (the
front page) into the Wiki within the appmaker
. This will provide
traversal a resource tree to work against when it attempts to
resolve URLs to resources.
We’re using a mini-framework callable named PersistentApplicationFinder
in our application (see __init__.py
). A PersistentApplicationFinder
accepts a ZODB URL as well as an “appmaker” callback. This callback
typically lives in the models.py
file. We’ll just change this function,
making the necessary edits.
Looking at the Result of Our Edits to models.py
¶
The result of all of our edits to models.py
will end up looking
something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from persistent import Persistent
from persistent.mapping import PersistentMapping
class Wiki(PersistentMapping):
__name__ = None
__parent__ = None
class Page(Persistent):
def __init__(self, data):
self.data = data
def appmaker(zodb_root):
if not 'app_root' in zodb_root:
app_root = Wiki()
frontpage = Page('This is the front page')
app_root['FrontPage'] = frontpage
frontpage.__name__ = 'FrontPage'
frontpage.__parent__ = app_root
zodb_root['app_root'] = app_root
import transaction
transaction.commit()
return zodb_root['app_root']
|
Removing View Configuration¶
In a previous step in this chapter, we removed the
tutorial.models.MyModel
class. However, our views.py
module still
attempts to import this class. Temporarily, we’ll change views.py
so
that it no longer references MyModel
by removing its imports and removing
a reference to it from the arguments passed to the @view_config
configuration decoration decorator which sits atop the my_view
view callable.
The result of all of our edits to views.py
will end up looking
something like this:
1 2 3 4 5 | from pyramid.view import view_config
@view_config(renderer='tutorial:templates/mytemplate.pt')
def my_view(request):
return {'project':'tutorial'}
|
Testing the Models¶
To make sure the code we just wrote works, we write tests for the model
classes and the appmaker. Changing tests.py
, we’ll write a separate test
class for each model class, and we’ll write a test class for the
appmaker
.
To do so, we’ll retain the tutorial.tests.ViewTests
class provided as a
result of the pyramid_zodb
project generator. We’ll add three test
classes: one for the Page
model named PageModelTests
, one for the
Wiki
model named WikiModelTests
, and one for the appmaker named
AppmakerTests
.
When we’re done changing tests.py
, it will look something like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | import unittest
from pyramid import testing
class PageModelTests(unittest.TestCase):
def _getTargetClass(self):
from tutorial.models import Page
return Page
def _makeOne(self, data=u'some data'):
return self._getTargetClass()(data=data)
def test_constructor(self):
instance = self._makeOne()
self.assertEqual(instance.data, u'some data')
class WikiModelTests(unittest.TestCase):
def _getTargetClass(self):
from tutorial.models import Wiki
return Wiki
def _makeOne(self):
return self._getTargetClass()()
def test_it(self):
wiki = self._makeOne()
self.assertEqual(wiki.__parent__, None)
self.assertEqual(wiki.__name__, None)
class AppmakerTests(unittest.TestCase):
def _callFUT(self, zodb_root):
from tutorial.models import appmaker
return appmaker(zodb_root)
def test_no_app_root(self):
root = {}
self._callFUT(root)
self.assertEqual(root['app_root']['FrontPage'].data,
'This is the front page')
def test_w_app_root(self):
app_root = object()
root = {'app_root': app_root}
self._callFUT(root)
self.failUnless(root['app_root'] is app_root)
class ViewTests(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
def tearDown(self):
testing.tearDown()
def test_my_view(self):
from tutorial.views import my_view
request = testing.DummyRequest()
info = my_view(request)
self.assertEqual(info['project'], 'tutorial')
|
Declaring Dependencies in Our setup.py
File¶
Our application now depends on packages which are not dependencies of the
original “tutorial” application as it was generated by the paster create
command. We’ll add these dependencies to our tutorial
package’s
setup.py
file by assigning these dependencies to both the
install_requires
and the tests_require
parameters to the setup
function. In particular, we require the docutils
package.
Our resulting setup.py
should look like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import os
from setuptools import setup, find_packages
here = os.path.abspath(os.path.dirname(__file__))
README = open(os.path.join(here, 'README.txt')).read()
CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
requires = [
'pyramid',
'repoze.zodbconn',
'repoze.tm2>=1.0b1', # default_commit_veto
'repoze.retry',
'ZODB3',
'WebError',
'docutils',
]
setup(name='tutorial',
version='0.0',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
"Intended Audience :: Developers",
"Framework :: Pylons",
"Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
],
author='',
author_email='',
url='',
keywords='web pylons pyramid',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=requires,
tests_require=requires,
test_suite="tutorial",
entry_points = """\
[paste.app_factory]
main = tutorial:main
""",
paster_plugins=['pyramid'],
)
|
Running the Tests¶
We can run these tests by using setup.py test
in the same way we
did in Running the Tests. Assuming our shell’s current working
directory is the “tutorial” distribution directory:
On UNIX:
$ ../bin/python setup.py test -q
On Windows:
c:\pyramidtut\tutorial> ..\Scripts\python setup.py test -q
The expected output is something like this:
.....
----------------------------------------------------------------------
Ran 5 tests in 0.008s
OK