mrtopf.de

Suche
Close this search box.

Using ToscaWidgets with repoze.bfg and Storm

Recently I tried to use Storm and ToscaWidgets with repoze.bfg to create a simple usermanagement application. Here is a little rundown of what I did.

First I installed repoze.bfg inside a virtualenv:

   mkdir users
   cd users
   virtualenv --no-site-packages .
   source bin/activate

   easy_install repoze.bfg

Some problems might occur on a Mac with Python 2.5 resulting in the need to install PasteDeploy and zope.proxy separately:

    easy_install PasteDeploy
    easy_install zope.proxy

Then you can create a project:

   paster create -t bfg usermanagement

Inside usermanagement you find files like models.py, views.py and so on which is pretty good explained in the bfg documentation.

We now also need to develop this egg:

    cd usermanagement
    python setup.py develop

The basic idea now is to have a UserFolder model which contains BaseUser objects. This led to changing models.py to look like this:

    from zope.interface import Interface
    from zope.interface import implements

    class IUserFolder(Interface):
        pass

    class UserFolder(object):
        implements(IUserFolder)


    class IBaseUser(Interface):
        pass

    class BaseUser(object):
        implements(IBaseUser)

    root = UserFolder()

    def get_root(environ):
        return root

I also changed configure.zcml to reflect that and both use the same default view:

    <configure xmlns="http://namespaces.zope.org/zope"
    	   xmlns:bfg="http://namespaces.repoze.org/bfg"
    	   i18n_domain="repoze.bfg">

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

      <bfg:view
         for=".models.IBaseUser"
         view=".views.my_view"
         />

      <bfg:view
         for=".models.IUserFolder"
         view=".views.my_view"
         />

    </configure>


Adding an addform

Now for adding an addform I wanted to use ToscaWidgets. I installed it manually for now (later this will go into setup.py):

    easy_install ToscaWidgets
    easy_install tw.forms
    easy_install genshi

The latter is needed because otherwise the internally used template engine is not found.

Then I added another function and some imports to views.py:

    from tw.forms import *
    from tw.forms.validators import *
    from tw.api import WidgetsList

    class UserAddForm(TableForm):
        # This WidgetsList is just a container
        class fields(WidgetsList):
            id = HiddenField(default="I'm hidden!")
            username = TextField()
            fullname = TextField()
            email = TextField(
                validator = Email()
                )
            password = PasswordField(
                validator = String(not_empty=True),
                max_size = 10
                )
            password_confirm = PasswordField(
                validator = String(not_empty=True),
                max_size=10
                )

    def addform_view(context, request):
        """show the addform for adding a new user"""
        form = UserAddForm('form')
        form_output = form.display(context)

        return render_template_to_response('templates/addform.pt',
                form=form_output)

and wired this as „addform“ into configure.zcml by adding:

    <bfg:view
        for=".models.IUserFolder"
        view=".views.addform_view"
        name="addform"
        />

I started the server by typing

    paster serve usermanagement.ini

inside the project directory and accessed at http://127.0.0.1:5432/addform

We get the following error:

    TypeError: No object (name: ToscaWidgets per-request storage) has been registered for this thread

Looking the example it becomes clear that some WSGI middleware is missing.

Guessing from examples for ToscaWidgets and reading some code I figured out I had to add something to run.py so that it looks like this:

    def make_app(global_config, **kw):
        # paster app config callback
        from repoze.bfg import make_app
        import tw.api

        import usermanagement
        from usermanagement.models import get_root
        app = make_app(get_root, usermanagement)

        app = tw.api.make_middleware(app, {
                    'toscawidgets.framework' : 'wsgi',
                    'toscawidgets.middleware.inject_resources' : True,
                })
        return app

    if __name__ == '__main__':
        from paste import httpserver
        app = make_app(None)
        httpserver.serve(app, host='0.0.0.0', port='5432')

I assume that you can also add stuff like this to usermanagement.ini and this probably is the preferred way of doing it but back then I wasn’t that much used to all the WSGI stuff.

What the middleware here does is also not completely clear to me (and I haven’t yet looked at the code). One part is apparently to make the above error go away which means that it probably injects something into the request. The other part is to inject CSS and JS links into the header. This is similar to the Plone ResourceRegistries the resource stuff in Zope3 ZCML in that each package (widgets mostly in this case) can register their JS and CSS snippets to be included in the document when being rendered.

I also wonder if it’s possible to get around adding this to the middleware stack.

When we now try the app again we notice that addform.pt is missing so we add it to templates:

    <html xmlns="http://www.w3.org/1999/xhtml"
         xmlns:tal="http://xml.zope.org/namespaces/tal">
    <head></head>
    <body>
      <h1>Add new user</h1>

      ${form}
    </body>
    </html>

And voila, we have our first form rendered:

The first form
The first form

Submitting the form

A form is nice but it’s nicer if you actually also store and validate the data. ToscaWidgets keeps the action attribute of the form empty (although I guess you can put something in there somehow) so it will submit to the same URL again.

The example shows how to handle it by checking for the POST method and I do the same just with WebOb which bfg uses. I changed my view to this:

    from formencode import Invalid
    def addform_view(context, request):
        """show the addform for adding a new user"""
        form = UserAddForm('form')
        if request.method=="POST":
            try:
                values = form.validate(request.POST)
                return render_template_to_response('templates/addform_success.pt',
                    values = values)
            except Invalid, error:
                pass
        form_output = form.display(context)
        return render_template_to_response('templates/addform.pt',
                form=form_output)

What’s happening here is basically a check if the request was a POST, then passing the values of the POST to the validate() method of the form and checking for an exception. If there was an exception we simply render the form again like before. If no exception was raised we display the addform_success.pt template:

    <html xmlns="http://www.w3.org/1999/xhtml"

         xmlns:tal="http://xml.zope.org/namespaces/tal">

    <head></head>
    <body>
      <h1>Added new user ${values.username}</h1>


    </body>
    </html>

Note how this is a little different from how Page Template syntax usually looks like. It’s different in so far in that z3c.pt also allows Expression interpolation inside HTML and not only attributes. See the z3c.pt docs (http://pypi.python.org/pypi/z3c.pt) for more information on what’s different.

Now if you pass some wrong data in you should see something like this:

A form with an error
A form with an error

If everything succeeded you can see confirmation we defined above.

So far this works but we are not finished yet. We of course need to store the data somewhere. In production we maybe also want to do some redirect or other means of preventing the form to be submitted twice by accident and e.g.
hitting the reload button on the browser. But that’s for later.

Storage

This is where Storm comes into play, a object relational database mapper which
is developed and used by Canonical. We install it by doing

    easy_install storm

Now we need a database which basically uses our user schema, namely username,
email, password and fullname. For this we create an sqlite database (which btw
you should have installed):

    sqlite3 users.db
    sqlite> CREATE TABLE users (id INTEGER PRIMARY KEY, username VARCHAR, password VARCHAR, fullname VARCHAR, email VARCHAR);
    .quit

Now that we have that table we can add some stuff to models.py to wire it to
the table. I replaced the BaseUser class in models.py with this one:


    class BaseUser(object):
        implements(IBaseUser)

        __storm_table__ = "users"
        id = Int(primary=True)
        username = Unicode()
        password = Unicode()
        fullname = Unicode()
        email = Unicode()

and imported some storm stuff on top:

    from storm.locals import *

We also need to create a database. I did this on module level directly after the import:

    database = create_database("sqlite:users.db")

Now the class should know about our database schema (btw, while I like the
fact that it does not use a metaclass I still would like to have the actual
database schema uncoupled from the actual class to have more flexibility.
Something a la ZPatterns (only old people like me know what this is) might be nice.

Now we need to add some code to views.py. The new view now looks like this:

    from storm.locals import *
    import models

    def addform_view(context, request):
        """show the addform for adding a new user"""
        form = UserAddForm('form')
        if request.method=="POST":
            try:
                values = form.validate(request.POST)

                # open the store
                store = Store(models.database)
                # create a new user and store the data inside
                user = models.BaseUser()
                user.username = unicode(values.get("username",""))
                user.email = unicode(values.get("email",""))
                user.password = unicode(values.get("password",""))
                user.fullname = unicode(values.get("fullname",""))
                # add it to the store
                store.add(user)
                # commit the transaction and flush the store
                store.commit()
                store.flush()
                # now close it again so we are thread safe
                store.close()
                # this is like before
                return render_template_to_response('templates/addform_success.pt',
                    values = values)
            except Invalid, error:
                print error
                pass
        form_output = form.display(context)
        return render_template_to_response('templates/addform.pt',
                form=form_output)

I had some trouble with sqlite and threading as it didn’t work when I defined
the store in models.py and just used it in the view. Thus I now open and close
the store in each transaction. This limitation might not apply to other
database backends though. The question nevertheless is for what database
backend you code so maybe this is safer but maybe also a litte less performant
(but I don’t know what the store compared to the database object actually
does).

If you now start the server again and add some users you should finally see them again inside the database:

    sqlite3 users.db
    sqlite> select * from users
    1|hansi|hdbjdb|Dummy|dummy@dummydummy123.com
    2|hansi2|hdbjdb|Dummy|dummy@dummydummy123.com
    3|foobar|foobar111|Dummy|dummy@dummydummy123.com

Listing users

The userfolder view looks rather boring right now, so why not making this a list of all users?

Let’s define the template first as templates/userfolder.pt:

    <html xmlns="http://www.w3.org/1999/xhtml"
         xmlns:tal="http://xml.zope.org/namespaces/tal">
    <head></head>
    <body>
      <h1>User Listing</h1>
      <ul>
          <tal:block repeat="user users">
            <li>
                <a href="/${user.id}">${user.username}</a>
            </li>
          </tal:block>
      </ul>

    </body>
    </html>

We also wire it inside configure.zcml by changing the default
view for the folder:

    <bfg:view
       for=".models.IUserFolder"
       view=".views.userlisting"
       />

(make sure you replace this and not just add it to the file)

And we implement userlisting in views.py as follows:

    def userlisting(context, request):
        """show a userlisting"""
        store = Store(models.database)
        users = list(store.find(models.BaseUser).order_by(models.BaseUser.username))
        return render_template_to_response('templates/userfolder.pt',
                users=users)

If you restart you will see the userlisting at
http://localhost:5432/ (as the userfolder is our root object at
the moment). The only problem is if you click on a user because there is no
traversal yet defined for this and bfg produces a 404.

Let’s change this by adding a __getitem__() to the BaseUser class:

    def __getitem__(self, id):
        store = Store(database)
        user = store.get(BaseUser, int(id))
        return user

After a restart we can already click on the users but the template does not
say that much about them. So we need a new one and also need to create a
separate view for them. I added the following to views.py:

    def userdetails(context, request):
        return render_template_to_response('templates/userdetails.pt',
                                           context = context)

And we need to create some template for it as templates/userdetails.pt:

    <html xmlns="http://www.w3.org/1999/xhtml"
         xmlns:tal="http://xml.zope.org/namespaces/tal">
    <head></head>
    <body>
      <h1>User Details for ${context.username}</h1>

      <table border="0" cellspacing="5" cellpadding="5">
        <tr>
            <td>Username:</td>
            <td tal:content="context.username"></td>
        </tr>
        <tr>
            <td>Fullname:</td>
            <td tal:content="context.fullname"></td>
        </tr>
        <tr>
            <td>E-Mail:</td>

            <td tal:content="context.email"></td>
        </tr>
      </table>
    </body>
    </html>

Now we just need to wire this view to the model, which means changing it in configure.zcml:

    <bfg:view
       for=".models.IBaseUser"
       view=".views.userdetails"
       />

And that’s all there is to it. Much is of course still missing, such as deleting. Next steps could be to abstract things a bit more to have some sort of schema which handles storage and form generation all in one.

Teile diesen Beitrag