/

Pyramid Traversal and MongoDB

March 27, 2011 at 10:06 PM | categories: codes | View Comments

This post will talk about creating a simple web application with Pyramid and MongoDB. It will also talk about using a traversal method to map a requested URL to a callable function (controller/method/view/etc) in your application code.

The App

Enter Troll, an anonymous board web application. It is a simple application that is similar to a blog app (post and comment). The features are:

  • Sorted by activity (recently commented posts come first).
  • Automatic removal for the least commented post (there is a maximum number of posts).
  • Lord Inglip summoning pedestal (read: recaptcha).

The non-features:

  • No pagination. Maybe later with webhelper. Post limit is set to 10 by default anyway.
  • No test.
  • etc

The app uses a single mongodb collection for posts. The comments are embedded to a post document.

Source code is available here.

The Stack

This app uses few technologies

Traversal

NOTE : Traversal is optional in Pyramid. You can still use URL dispatch (pattern matching thing).

Traversal is a method of matching a requested URL to your application code just like a more familiar method, URL parsing and comparing it to a set of patterns. Traversal requires you to build a resource tree which is probably analogous to a file-system hierarchy. Pyramid will take a URL, and then traverse your resource tree trying to find a resource for that URL. Once a resource is found, Pyramid will try to find a function associated with that resource. The resource object found (or last traversed) is called context.

This post gave me a good idea about traversal.

For this application, two kinds of resource object are needed. The first one is the root object. This object will act as a container to post objects.

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

    def __init__(self, request):
        self.collection = request.db.post

    def __getitem__(self, name):
        post = Post(self.collection.find_one(dict(_id=ObjectId(name))))
        return _assign(post, name, self)

    def __len__(self):
        return self.collection.count()

    def __iter__(self):
        return ( _assign(Post(x), str(x['_id']), self) for x in self.collection.find().sort('updated', DESCENDING) )

The __getitem__ method will return post object (the child of the root object). The root object contains all posts. _assign is just a simple function to set some attributes of resource object.

def _assign(obj, name, parent):
    obj.__name__ = name
    obj.__parent__ = parent
    return obj

Next is the post object. This object is a slightly modified python dictionary (pymongo returns mongodb document as a python dict). Resource tree objects need to have a __parent__ attribute.

class Post(dict):
    def __init__(self, a_dict):
        super(Post, self).__init__(self)
        self.update(a_dict)
        self.__name__ = None
        self.__parent__ = None

The Views

This is the function for viewing a resource object.

@view_config(renderer='single.html', context=resources.Post)
@view_config(renderer='index.html', context=resources.Root)
def view(context, request):
    form = TrollForm()
    return {'p': context, 'form': form}

Pyramid allows you to write a function once and then register it multiple times for different contexts. Coming up is the function for handling post and comment addition.

@view_config(name='add', request_method='POST', context=resources.Post)
@view_config(name='add', request_method='POST', context=resources.Root)
def add(context, request):
    author = request.params['name']
    content = request.params['content']
    _add(context, author, content)
    return HTTPFound(location=request.resource_url(context))

Here is the _add function.

def _add(context, author, content):
    if context.__parent__ is None:
        _post(context.collection,
              author,
              content)
    else:
        _comment(context.__parent__.collection,
                 context['_id'],
                 author,
                 content)

Finally, doing insert/upsert to MongoDB.

def _post(collection, author, content):
    p = dict(author=author,
             content=content,
             comments=[],
             updated=datetime.utcnow(),
             time=datetime.utcnow())
    collection.insert(p)
    #remove unpopular post if >  10
    if collection.find().count() > 10:
        collection.remove({'_id': [x for x in collection.find().sort("updated", DESCENDING)][-1]['_id']})


def _comment(collection, post_id, author, comment):
    post = collection.find_one(dict(_id=post_id))
    time = datetime.utcnow()
    post['comments'].append(dict(author=author,
                                 comment=comment,
                                 time=time))
    post.update(dict(updated=time))
    collection.save(post)

Templating

Post form and comment form have the same fields (author, content, and captcha) and use the same form class (from wtforms). To prevent typing the same thing in many places, I created a template macro.

<%def name="createform(c, form)">
    <% link = request.resource_url(c)%>
    <form method="POST" action="${link}@@add">
        <div> ${form.name.label}: ${form.name(size=50)}</div>
        <div> ${form.content.label}: ${form.content(rows=5, cols=50)}</div>
        ${form.captcha}
        <input type="submit" value="Submit!"/>
    </form>
</%def>

This macro takes a context and a form object, to generate an html form. URL for any resource can easily be retrieved via resource_url method on request object. The '@@' means the start of a view name. Pyramid will traverse the URL until '@@', and search for a view named 'add' for that context.

This is how to use it.

${createform(request.context, form)}

If you want to use it on another template file, import it first.

<%namespace file="base.html" import="createform" />

Conclusion

Pyramid is a fun framework to tinker with.

Source Code

blog comments powered by Disqus

Content is licensed under Creative Commons License
Feed: entries, comments
Contact: bitbucket, github