Section 5.5. Selecting Categories

5.5. Selecting Categories

It's not complicated to add another widget to our form which displays and updates categories for us. There are several ways to do this: We can use a list of check boxes, a series of clickable links, or some kind of Ajaxified Web 2.0 drag-and-drop interface. But, our users probably already know how to use a multiple select field, and it's easy to add, so let's start with that.

We'll do this in two phases. The first is the easiest; we wire up the widget into our form and make it display the existing categories for each of the bookmarks in our database. The second part, which is really not difficult either, is to update the contents of our database based on the user's selection.

So far, the only thing we've used is text fields, so we didn't have to define any user options; but we want our select widget to contain a list of all the categories in the database, so we must get those category options and then feed it into the widget somehow.

We have several ways to do this:

  • We can define the options at widget instantiation time (and the widget will use the same options every time it's displayed).

  • We can pass in the options at widget rendering time (so that the widget can dynamically hand an options list to our widget).

  • We can assign a callable, which the widget will use to get the options dynamically.

All three of these methods can be incredibly useful. For instance, if I have a widget that's always going to display a static list of options, I just pass it in at instantiation time, and then I never have to worry about it; and it's incredibly easy to just reuse that widget in another form. On the other hand, if I want my list of options dynamically updated based on current database values, it makes a lot of sense to encapsulate the logic for getting the options list in a single function that the widget can call to get the list when it needs it. With either of these two methods, I don't even have to think about building the options list when I'm displaying my form; they're just there automatically.

But there are also times, particularly when I have complex context-sensitive logic to determine the options needed, when the second method is easiest.

Whereas Python has the "one (and preferably only one) obvious way to do it" philosophy, there are important use cases for each of these three widget option definition methods.

In our case, we want a list of options, but we want it to be updated whenever a new option is added to our application. So, this is an obvious time to use a callable to define our options list:

def get_category_options():     categories =     options = []     for category in categories:         options.append(category.ID, category.categoryName)     return options

A MultipleSelectField widget expects to receive a list of two value tuples, one tuple for each item you could select. The tuples should be in the format (1, "First Option"), where the first item in the tuple defines the value returned when that option is selected, and the second option, the string, defines the value that is displayed for that option. So, the preceding code just gets a list of categories from the database and iterates over that list appending a tuple with the id and categoryName of each record.

We could also use Python's list comprehension syntax to make this even simpler:

def get_category_options():     categories = [(category.ID, category.categoryName)                    for category in]     return categories

Next, we update our bookmark_fields list with a categories widget:

class bookmark_fields(widgets.WidgetsList):     bookmark_name = widgets.TextField(validator=validators.NotEmpty)     link = widgets.TextField(validator=validators.URL)     description = widgets.TextArea(validator=validators.NotEmpty)     categories = widgets.MultipleSelectField(options=get_category_options)h

If you're defining the options at render time, the MultipleSelect field won't be able to guess what kind of elements you're going to pass it (and expect to get back out of it)! Rather than guess, it will warn you that you need to define a validator. Remember, validators don't exist just to enforce validation rules; they also convert the HTTP POST string results into valid Python variables of the type you expectso if the widget can't figure out what you'll be expecting at the end, it's not going to be able to convert it for you. And rather than make some bad guess that gives you an error way down the road, the widget sensibly decides to fail early, right at instantiation time.

Okay, so now we have a MultipleSelect field in our form, and you can fire up your browser and go to http://localhost:8080/bookmarks/edit/1 to see it in action.

But what we really want is to have existing categories preselected for our users so that they can see what's in the database and update it. Widgets make this easy, too; as you've already seen in the earlier example, we can pass in a value at render time. In this case, all we need is a list of the index fields for each category that this bookmark belongs to.

Actually, you can define the display value for a widget at instantiation time, with a callable, or at render timejust like the options list. So, you have total flexibility, and all the widget parameters work the same way. More often than not, however, you are going to want to define the default values at render time.

Here's the new controller code for our bookmark editing form:

@expose(template="bookmarker.templates.form")     def bookmark(self, *args, **kwargs):         from sqlobject import SQLObject NotFound         if args and args[0] == "add":             values = ""             submit_action= "/save_bookmark/"         if args and args[0] == "edit":             try:                 b = Bookmarks.get(args[1])                 default_options = []                 for a in b.categories:                     default_options.append(                 values = {"bookmarkName" : b.bookmarkName,                           "link":,                           "description" : b.description,                           "categories": default_options}                except SQLObjectNotFound                    values = ""                    turbogears.flash = ("That's not a valid Bookmark, " +                                        "do you want to add one now?")         submit_action = "/save_bookmark/edit/%s" %args[1]     return dict(form=bookmark_form, values=values, action=submit_action)

The new code here is in lines 10-13, and all we're doing is creating a list of the ID values of each of the Categories associated with the Bookmarks object that we're editing. We then just put the default_options into our values dictionary under the select_categories key. We don't even have to edit our templatejust fire up your browser and take a look; it should "just work."

But we don't just want to look at the category values, we want to edit them. So, let's make a couple of simple changes to our save_bookmark method:

@expose() @turbogears.error_handler(bookmark) @turbogears.validate(form=bookmark_form) def save_bookmark(self, *args, **kwargs):     try:         b=Bookmarks.get(args[1])         b.bookmarkName=kwargs["bookmark_name"] = kwargs["link"]         b.description = kwargs["description"]         Bookmarks.updateCategories(b, kwargs["select_categories"])     except:         b=Bookmarks(bookmarkName=kwargs["bookmark_name"],                     link = kwargs["link"],                     description = kwargs["description"])         for item in kwargs["select_categories"]:             b.addCategories(Categories.get(item))     raise redirect("/index")

In the TRy clause, we're updating an existing object, so we have to delete existing relationships as well as add new ones. In the except clause, we know the bookmark doesn't exist yet, so our job is simpler; we're just adding new relationships. To do that, we iterate over the list of integer values we got back from our select_categories widget, and then use the automatically generated SQLObject addCagetories method to add these relationships to the database.

Is updateCategories another automatically generated method created for us by SQLObject? No; instead of adding a bunch of functionality to our controller to check for existing relationships and adding or deleting when necessary, we're creating an updateCategories method in the Bookmarks class.

The ability to move complex logic out of the controller and into the model is critical to maintaining a clean model-view-controller (MVC) design.

SQLObject makes this easy. Our Bookmarks class is just a plain old Python class that can take new methods whenever you need them. So, now that we've added updateCategories the Bookmarks class in our model looks like this:

class Bookmarks(SQLObject):     bookmarkName   = StringCol(alternateID=True, length=100)     link           = StringCol()     description    = StringCol()     categories     = RelatedJoin('Categories')     def updateCategories(some_bookmark, new_categories):         for existing_category in some_bookmark.categories:             some_bookmark.removeCategories(existing_category)         for each_category in new_categories:             some_bookmark.addCategories(each_category)

The updateCategories method takes a Bookmark object and a list of integers. It removes all the existing category relationships from the database for that bookmark and adds a new Category for each integer in the new_categories list. Because we passed our select object the ID values of our category records, and we got back a list of the ID values that were selected, there's nothing special we need to do here. We can add the relationship by passing the ID value in to the addCategories method. Before we do that, we remove all the preexisting relationships so that what we're left with is just the new values returned from the form.

Rapid Web Applications with TurboGears(c) Using Python to Create Ajax-Powered Sites
Rapid Web Applications with TurboGears: Using Python to Create Ajax-Powered Sites
ISBN: 0132433885
EAN: 2147483647
Year: 2006
Pages: 202

Similar book on Amazon © 2008-2017.
If you may any questions please contact us: