A few rules I try to follow with TurboGears

These are a few of the rules I try to follow in my design. So far, they’ve helped me out.

I aim to finish all interaction with the database before I get to the template layer.

This is non-trivial because it is so easy to forget that a method or an attribute will evaluate into a query. I use this rule because it lets me be certain about the number of interactions each page will have with the database.

I avoid branching (if-else clause) in my templates as much as possible.

I have a really hard time detangling code when I find a bunch of nested if statements. For all but the most trivial instances, I prefer to have a bunch of similar templates and then choose the best one. For example, instead of handling both a successful login and a failed login in a single template, I’ll make two different files and then choose the right one in my controller.

In practice, I have some really similar templates. But then I go back and strip out as much of the common code as possible and put those into widgets.

Any time I find a select() call in my controller, I consider making a new method in my model.

When I write something like this in a controller:

bluebirds = model.Bird.select(Bird.q.color == 'blue')

I usually come back later and put in something like this into the Bird class:

class Bird(SQLObject):
color = UnicodeCol()

@classmethod
def by_color(cls, color)
return cls.select(cls.q.color == color)

Now I have something that I can reuse. If I’m feeling whimsical I’ll use functools.partial to do something like this:

class Bird(SQLObject):
color = UnicodeCol()

def by_color(self, color):
return self.select(self.q.color == color)

redbirds = classmethod(partial(by_color, color='red'))
bluebirds = classmethod(partial(by_color, color='blue'))

Sidenote: I couldn’t figure out how to use the @classmethod decorator in the second version of by_color because partial complained. Appararently, callable(some_class_method) returns False, and partial requires the first argument to be a callable.

Maybe a reader can explain to me what’s going on there…

Published by

matt

My name is Matt Wilson and I live in Cleveland Heights, Ohio. I love random emails from strangers, so get in touch! matt@tplus1.com.

  • Chris

    Concerning the sidenote, this is because an unbound classmethod is indeed not callable (it only wraps a callable that you need to __get__ first). A workaround would be adding by_color=classmethod(by_color) at the end of the class declaration.

  • Chris, thanks for the comments! If I understand you right, instead of

    redbirds = classmethod(partial(by_color, color='red'))
    bluebirds = classmethod(partial(by_color, color='blue'))

    I could do:

    by_color = classmethod(by_color)
    redbirds = partial(by_color, color='red')
    bluebirds = partial(by_color, color='blue')

    I haven’t tried it out, but I like that I don’t need to keep using classmethod over and over again in your approach.

  • Your first rule seems very tough to meet. Wouldn’t that mean that you need to flatten any generator for select results to a list and also do the same for any attribute capsuling a 1:n or n:m-relation?

    I don’t see the advantage of this. If you want to see the queries executed by your page controller, turn on SQLObject/SQLAlchemy debugging. Anyway, there’s no real difference between controller and template processing in this respect. They both happen in your controller method, only the template rendering is hidden in the “expose” decorator.

  • Christopher Arndt: Most of the time, I don’t care about coercing into a list in the controller before the template because like you point out, it’s the same cost if I do it in the controller vs the template.

    However, in templates where I iterate through the results twice, it does make sense to coerce the selectResults into a list. Otherwise, I’ll run the exact same query twice:

    In [24]: User.select(User.q.user_name.startswith('Z'))

    Out[24]:

    In [25]: list(_24)
    1/Select : SELECT tg_user.id, tg_user.modifieddate, tg_user.createddate, tg_user.supervisor_id, tg_user.first_name, tg_user.last_name, tg_user.v2organization_id, tg_user.participationstatus_id, tg_user.user_name, tg_user.email_address, tg_user.password, tg_user.mobilenumber, tg_user.homenumber FROM tg_user WHERE ((tg_user.user_name) LIKE ('Z%'))
    1/QueryR : SELECT tg_user.id, tg_user.modifieddate, tg_user.createddate, tg_user.supervisor_id, tg_user.first_name, tg_user.last_name, tg_user.v2organization_id, tg_user.participationstatus_id, tg_user.user_name, tg_user.email_address, tg_user.password, tg_user.mobilenumber, tg_user.homenumber FROM tg_user WHERE ((tg_user.user_name) LIKE ('Z%'))

    Out[25]: []

    In [26]: list(_24)
    1/Select : SELECT tg_user.id, tg_user.modifieddate, tg_user.createddate, tg_user.supervisor_id, tg_user.first_name, tg_user.last_name, tg_user.v2organization_id, tg_user.participationstatus_id, tg_user.user_name, tg_user.email_address, tg_user.password, tg_user.mobilenumber, tg_user.homenumber FROM tg_user WHERE ((tg_user.user_name) LIKE ('Z%'))
    1/QueryR : SELECT tg_user.id, tg_user.modifieddate, tg_user.createddate, tg_user.supervisor_id, tg_user.first_name, tg_user.last_name, tg_user.v2organization_id, tg_user.participationstatus_id, tg_user.user_name, tg_user.email_address, tg_user.password, tg_user.mobilenumber, tg_user.homenumber FROM tg_user WHERE ((tg_user.user_name) LIKE ('Z%'))

    Out[26]: []

    However, what I really care about is making sure that I don’t follow any multiplejoins or related joins once I’m already in the template. So instead of doing something like this in my template:

    for employee in employees:
    for task in employee.tasks: # tasks are a related join, and this does a query every time.

    I’ll build a dictionary in my controller that maps employees to tasks:

    for employee in employees;
    for task in employee_tasks[employee]

    The rule of thumb about making sure everything is already out of the database before I get to the template is just a simple way of avoiding the previous two scenarios.