In terms of dynamicism, the worst cases I've explained so far in Gibe are the little comment format hack to allow the use of Postmarkup instead of HTML in comments, and the adding of little trivial plugins to add visual widgets at the top and bottom of blog entries.  Certainly not rocket science.  And, well, neither is this...

My next task was to look at how one could add additional fields to the blog entry create/edit screen - to allow plugins to ask for additional information like tags, geographical location, your mood, what you're currently listening to, and other vain things that nobody really cares about (I mean, it's a blog, it's not like it's useful...).

I used tags as my test case, since that is at least something I can see some value in, and it's something that's already around except for the actual entry of the tags.  Until now, I've been manually typing in things like:

INSERT INTO post_tag
    SELECT 625 AS post_id, tag_id FROM tag
        WHERE name IN
            ('gibe', 'python', 'code','amatomu',
            'blogs','me','tgsociable');

So, back to the simplest of Plugin classes:

class Plugin(object):
    def post_top_widgets(self, post, widgets, context):
        # widgets to put above the post
        pass

    def post_bottom_widgets(self, post, widgets, context):
        # widgets to put below the post
        pass

We need to add methods for:

  • getting the necessary TurboGears (and, later, ToscaWidgets) widgets to add to the blog entry create/edit
  • providing the necessary values for the particular post being editing for the widgets added
  • handling the values coming back from the form - ie, saving them.
    def post_edit_widgets(self, blog, widgets, context):
        # widgets to add to post create/edit admin page
        pass

    def post_edit_values(self, blog, post, values, context):
        # widgets to add to post create/edit admin page
        pass
    
    def post_edit_handle(self, blog, post, form_values, context):
        # post might not exist yet
        pass

    def post_edit_handle_after(self, blog, post, form_values, context):
        # post will exist now
        pass

Next, we need to have helper methods to run through all the plugins and do all this stuff in a single command:

def post_edit_widgets(blog, context):
    widgets_list = []
    for w in PostFormFields():
        widgets_list.append(w)

    for k, v in plugins.items():
        v.post_edit_widgets(blog, widgets_list, context)

    return widgets_list

def post_edit_values(blog, post, context):
    values = {}
    if post:
        values['title'] = post.title
        values['content'] = post.content

    for k, v in plugins.items():
        v.post_edit_values(blog, post, values, context)
    return values

def post_edit_handle_after(blog, post, kw, context):
    for k, v in plugins.items():
        v.post_edit_handle_after(blog, post, kw, context)

The PostFormFields class was my original WidgetList of widgets in the Blog Entry create/edit page.  This is then passed through each plugin's post_edit_widgets method.  Similarly, post_edit_values provides values for each widget - since the original PostFormFields widgets are hardcoded, the values are hardcoded here.  post_edit_handle_after is called after the post has been created from the title and content, so it only needs to deal with the plugins.

Implementation of the plugin part for tags is rather trivial:

tepw = TagEditPostWidget(name='tageditpostwidget', label="Tags")
class TagsPlugin(Plugin):
    def post_edit_widgets(self, blog, widget_list, context):
        for i, w in enumerate(widget_list):
            if w.name == "title":
                widget_list.insert(i+1, tepw)

    def post_edit_values(self, blog, post, values, context):
        if post:
            values['tageditpostwidget'] = post.tags

    def post_edit_handle_after(self, blog, post, form_values, context):
        post.tags = form_values['tageditpostwidget']

It looks really simple, because most of the hard work is done by the TagEditPostWidget widget, and the validator for that widget.

The widget itself is a text input field that is made into a tokenised autocompleter using script.aculo.us (by just using the Scriptaculous package to provide a TurboGears widget that bundles it).  I probably didn't gain all that much by inheriting from the TextField widget from TurboGears, but it seemed the best one to do so from:

class TagEditPostWidget(widgets.TextField):
    template = """
<div xmlns:py="http://purl.org/kid/ns#">
    <input
        autocomplete="off"
        type="text"
        name="${name}"
        class="${field_class}"
        id="${field_id}"
        value="${value}"
        size="50"
        py:attrs="attrs"
    />
   <div class="auto_complete" id="${field_id}_ac"></div>
   <script type="text/javascript">
   autocomplete_${field_id} = ${options_json};
   new Autocompleter.Local('${field_id}', '${field_id}_ac', autocomplete_${field_id}, ${autocompleter_options});
   </script>
</div>
"""
    params_doc = {
        'autocompleter_options': 'Options for the autocompleter',
    }
    params = params_doc.keys()

    autocompleter_options = {
        'tokens': [','],
        'frequency': 0.001,
    }

    validator = TagFieldValidator
    list_attrs = {'class': 'tageditlist',}
    css = [
        widgets.CSSLink(resource_name, 'css/tags.css'),
    ]
    javascript = [
        scriptaculous.prototype_js,
        scriptaculous.scriptaculous_js,
        scriptaculous.effects_js,
        scriptaculous.controls_js,
    ]
    def update_params(self, d):
        d['options'] = [(tag.tag_id, tag.display_name) for tag in model.Tag.select(order_by = 'display_name')]
        super(TagEditPostWidget, self).update_params(d)
        d['options_json'] = simplejson.dumps([tag_display_name for tag_id, tag_display_name in d['options']])
        d['autocompleter_options'] = simplejson.dumps(d['autocompleter_options'])

You'll note that we don't seem to deal with values at all - it seems as if we'd be dealing with strings from the form, and we'd be providing a string to the form to display it.  And, when you look at the post_edit_values and post_edit_handle_after, it's dealing with tags.

It's the TagFieldValidator that is doing the conversion from a comma-separated string submitted from the form into a list of Tag objects, and the other way around for a list of Tag objects to a comma-separated string when displaying the form:

class TagFieldValidator(validators.String):
    def _to_python(self, value, state):
        tagnames = value.split(",")
        tagnames = [tagname.strip() for tagname in tagnames if tagname.strip()]
        tags = []
        for tagname in tagnames:
            t = model.Tag.select_by(name=tagname)
            if t:
                t = t[0]
            if not t:
                t = model.Tag(name=tagname, display_name=tagname)
            tags.append(t)
        return tags

    def _from_python(self, value, state):
        tags = value
        return ", ".join([t.name for t in tags])

The _from_python is simple, converting a list of existing tags to a comma-separated string.  The _to_python is harder, since we have to create Tag objects that don't already exist.  And, as you can see from edit_handle_after, it's worth it to just not have to worry about it in other locations.