Storing content in the ZODB

Goals

  • Create a CRUD app that adds records to persistent storage.

Warning

Caveat: this will likely only work on systems that have C compilation tools installed (XCode, Linux) or on Windows systems. If you can’t get ZODB installed properly you may need to pair up with someone who can.

Objectives

  • Install deform.
  • Install and include pyramid_tm.
  • Install and include pyramid_zodbconn.
  • Make our “content” classes inherit from Persistent.
  • Set up a database connection string in our application.
  • Set up a root factory that serves the root from ZODB rather than from memory.

Steps

  1. easy_install pyramid_zodbconn pyramid_tm deform.

  2. mkdir zodb

  3. Copy the following into zodb/application.py:

     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
    from wsgiref.simple_server import make_server
    
    from pyramid.config import Configurator
    from pyramid_zodbconn import get_connection
    
    from resources import bootstrap
    
    
    def root_factory(request):
        conn = get_connection(request)
        return bootstrap(conn.root())
    
    
    def main():
        settings = {"zodbconn.uri": "file://Data.fs"}
        config = Configurator(root_factory=root_factory, settings=settings)
        config.include("pyramid_zodbconn")
        config.include("pyramid_tm")
        config.add_static_view('static', 'deform:static')
        config.scan("views")
        app = config.make_wsgi_app()
        return app
    
    
    if __name__ == '__main__':
        app = main()
        server = make_server(host='0.0.0.0', port=8080, app=app)
        server.serve_forever()
    
  4. Copy the following into zodb/views.py:

     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
    62
    63
    64
    65
    66
    67
    68
    69
    70
    from random import randint
    
    from pyramid.httpexceptions import HTTPFound
    from pyramid.view import view_config
    
    import colander
    from deform import Form
    from deform.widget import TextAreaWidget
    
    from resources import Folder
    from resources import Document
    
    
    class FolderSchema(colander.Schema):
        title = colander.SchemaNode(colander.String())
    
    
    class DocumentSchema(colander.Schema):
        title = colander.SchemaNode(colander.String())
        content = colander.SchemaNode(colander.String(), widget=TextAreaWidget())
    
    
    class ProjectorViews(object):
        def __init__(self, context, request):
            self.context = context
            self.request = request
    
        @view_config(renderer="templates/folder_view.pt", context=Folder)
        def folder_view(self):
            return {}
    
        @view_config(name="add_folder", context=Folder, renderer="templates/form.pt")
        def add_folder(self):
            schema = FolderSchema()
            form = Form(schema, buttons=('submit',))
            if 'submit' in self.request.POST:
                # Make a new Folder
                title = self.request.POST['title']
                name = str(randint(0,999999))
                new_folder = Folder(title)
                new_folder.__name__ = name
                new_folder.__parent__ = self.context
                self.context[name] = new_folder
                # Redirect to the new folder
                url = self.request.resource_url(new_folder)
                return HTTPFound(location=url)
            return {"form": form.render()}
    
        @view_config(name="add_document", context=Folder, renderer="templates/form.pt")
        def add_document(self):
            schema = DocumentSchema()
            form = Form(schema, buttons=('submit',))
            if 'submit' in self.request.POST:
                # Make a new Document
                title = self.request.POST['title']
                content = self.request.POST['content']
                name = str(randint(0,999999))
                new_document = Document(title, content)
                new_document.__name__ = name
                new_document.__parent__ = self.context
                self.context[name] = new_document
                # Redirect to the new document
                url = self.request.resource_url(new_document)
                return HTTPFound(location=url)
            return {"form": form.render()}
    
        @view_config(renderer="templates/document_view.pt",
                     context=Document)
        def document_view(self):
            return {}
    
  5. Copy the following into zodb/resources.py:

     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
    from persistent import Persistent
    from persistent.mapping import PersistentMapping
    import transaction
    
    class Folder(PersistentMapping):
        def __init__(self, title):
            super(Folder, self).__init__()
            self.title = title
    
    
    class SiteFolder(Folder):
        __name__ = None
        __parent__ = None
    
    
    class Document(Persistent):
        def __init__(self, title, content):
            self.title = title
            self.content = content
    
    
    def bootstrap(zodb_root):
        if not 'projector' in zodb_root:
            root = SiteFolder('Projector Site')
            zodb_root['projector'] = root
            transaction.commit()
        return zodb_root['projector']
    
  6. $ python application.py

  7. Open http://127.0.0.1:8080/ in your browser.

  8. Add folders and documents.

Extra Credit

  • Create a view that deletes a document.
  • Remove the configuration line that includes pyramid_tm. What happens when you restart the application? Are your changes persisted across restarts?
  • What happens if you delete the files named Data.fs*.

Analysis

We install pyramid_zodbconn to handle database connections to ZODB. This pulls the ZODB3 package as well.

To enable pyramid_zodbconn:

  • We activate the package configuration using config.include.
  • We define a zodbconn.uri setting with the path to the Data.fs file.
  • We put that setting in a dict and pass it to the Configurator.

In the root factory, instead of using our old root object, we now get a connection to the ZODB and create the object using that.

Our resources need a couple of small changes. Folders now inherit from persistent.PersistentMapping and document from persistent.Persistent. Note that Folder now needs to call super() on the __init__ method, or the mapping will not initialize properly.

On the bootstrap, note the use of transaction.commit() to commit the change.

pyramid_tm provides us with automatic transaction handling (one transaction per request). As a result, we don’t need to commit a transaction by hand.

We use the Deform package to autogenerate HTML forms.

Discussion

  • Note that you can add a folder to a folder, or a document to a folder.
  • How does pyramid_tm work? Why is centralized, automatic transaction management a good thing?

Table Of Contents