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