Tutorial

 
Return to main page

BFG + Storm = CRUD

Author: tseaver
Date: 06/18/09 at 16:03:48
External URL: http://svn.repoze.org/playground/tseaver/stormy

 ()

I have just finished a prototype of a traverseal-based RDBMS application using the Storm object-relational mapper.

The models here are a Person class (corresponding to a single row in the persons table, as defined in the Storm tutorial), and a traversable container class, Persons. The Persons class defines __getitem__ (making it traversable with the primary key), as well as __iter__ and __delitem__.

The root Persons object holds onto a "store" object, managed using the storm.zope.zstorm:ZStorm class to hand out per-thread copies; this class also arranges for the stores to be associated with the Zope transaction model.

The app expects to use repoze.tm2 to manage per-request transactions, and registers a "commit veto" hook which prevents error responses from committing a transaction.

The views are straightforward BFG views: note that they do not do any explicit transaction management. They demonstrate using a "main template" which holds the common look-and-feel for the application.

  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
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#------------------------------------------------------------------------------
# stormy/models.py
#------------------------------------------------------------------------------
from storm.locals import create_database
from storm.locals import Int
from storm.locals import Store
from storm.locals import Unicode
from storm.zope.zstorm import global_zstorm

class Person(object):
    __storm_table__ = 'persons'
    id = Int(primary=True)
    name = Unicode()

class Persons(object):
    __parent__ = None
    __name__ = None

    def __init__(self, store):
        self.store = store

    def __getitem__(self, id):
        try:
            id = int(id)
        except (TypeError, ValueError):
            raise KeyError(id)
        person =  self.store.get(Person, id)
        if person is None:
            raise KeyError(id)
        person.__parent__ = self
        person.__name__ = str(id)
        return person

    def __delitem__(self, id):
        try:
            id = int(id)
        except (TypeError, ValueError):
            raise KeyError(id)
        found = self.store.find(Person, id=id)
        if found.count() == 0:
            raise KeyError(id)
        assert found.count() == 1
        self.store.remove(found.any())

    def __iter__(self):
        for person in self.store.find(Person).order_by(Person.name):
            person.__parent__ = self
            person.__name__ = str(person.id)
            yield person

DEFAULT_URI = 'sqlite:stormy.db'

def _make_store(name, db_uri=DEFAULT_URI):
    store = global_zstorm.get(name, db_uri)
    found = store.execute('select * from sqlite_master '
                          'where type = "table" and name = "persons"'
                        ).get_all()
    if not found:
        store.execute('create table persons'
                      '( id integer primary key'
                      ', name varchar(1024) unique'
                      ')', noresult=True)

        store.commit()
    return store

def get_root(environ, name='main', db_uri=DEFAULT_URI):
    store = environ.get('storm.store') # middlestorm uses this key
    if store is None:
        store = _make_store(name, db_uri)
    return Persons(store)

#------------------------------------------------------------------------------
# stormy/views.py
#------------------------------------------------------------------------------
from repoze.bfg.chameleon_zpt import get_template
from repoze.bfg.chameleon_zpt import render_template_to_response
from repoze.bfg.url import model_url
from repoze.bfg.view import static
from webob.exc import HTTPFound

from stormy.models import Person

static_view = static('templates/static')

def view_root(context, request):
    persons = [{'id': x.id, 'name': x.name} for x in context]
    return render_template_to_response(
                'templates/view_root.pt',
                main_template = get_template('templates/main_template.pt'),
                persons = persons,
                status = request.GET.get('status'),
                here = model_url(context, request),
               )

def search_persons(context, request):
    name = request.GET.get('name')
    if name is None or name.strip() == '':
        return HTTPFound(location = model_url(context, request,
                                      query={'status': 'No search term'}))
    persons = context.store.find(Person, Person.name.like(u'%%%s%%' % name))
    if persons.count() == 0:
        status = 'No persons matched "%s"' % name
    else:
        status = request.GET.get('status')
    return render_template_to_response(
                'templates/search_persons.pt',
                main_template = get_template('templates/main_template.pt'),
                persons = persons,
                name = name,
                status = status,
                here = model_url(context, request),
               )


def add_person(context, request):

    if 'form.submitted' in request.POST:
        person = Person()
        name = person.name = request.POST['name']
        context.store.add(person)
        return HTTPFound(
                location = model_url(context, request,
                            query={'status': 'Person added: %s' % name}))

    return render_template_to_response(
                'templates/add_person.pt',
                main_template = get_template('templates/main_template.pt'),
                here = model_url(context, request),
                status = request.GET.get('status'),
                save_url = model_url(context, request, request.view_name),
               )

def delete_person(context, request):
    if 'confirm' in request.params:
        id = request.params['id']
        name = context[id].name
        del context[id]
        return HTTPFound(
                location = model_url(context, request,
                            query={'status': 'Person deleted: %s' % name}))

    child = context[request.params['id']]
    return render_template_to_response(
                'templates/delete_person.pt',
                main_template = get_template('templates/main_template.pt'),
                name = child.name,
                status = request.GET.get('status'),
                here = model_url(context, request),
                confirm_url = model_url(context, request, request.view_name,
                                        query={'id': child.__name__,
                                               'confirm': 1}),
               )

def view_person(context, request):
    return render_template_to_response(
                'templates/view_person.pt',
                main_template = get_template('templates/main_template.pt'),
                name = context.name,
                status = request.GET.get('status'),
                here = model_url(context.__parent__, request),
               )


def commit_veto(environ, status, headers):
    # Hook for repoze.tm2:  veto commit of error responses.
    return status.startswith('4') or status.startswith('5')

#------------------------------------------------------------------------------
# stormy/configure.zcml
#------------------------------------------------------------------------------
<configure xmlns="http://namespaces.repoze.org/bfg">

  <!-- this must be included for the view declarations to work -->
  <include package="repoze.bfg.includes" />

  <view
     for=".models.Persons"
     view=".views.view_root"
     />

  <view
     for=".models.Persons"
     view=".views.search_persons"
     name="search.html"
     />

  <view
     for=".models.Persons"
     view=".views.add_person"
     name="add.html"
     />

  <view
     for=".models.Persons"
     view=".views.delete_person"
     name="delete.html"
     />

  <view
     for=".models.Persons"
     view=".views.static_view"
     name="static"
     />

  <view
     for=".models.Person"
     view=".views.view_person"
     />

</configure>