27.6.05. Success! Zope 3, add views, and Ajax

Alright! Finally, I have the first stage of my snippets library for Zope 3 rolling. It’s not displaying much at the moment, but it is doing what I’ve been trying to do.

  • It uses Ajax to load an add form into the index page the first time ‘add new…’ is clicked. After that, clicking on the ‘add new’ button toggles the add form.
  • The add form uses Zope 3’s widget system, and uses the built in addform system. I made a custom template that was a bit more stripped down while still using Zope 3’s widget macros and other facilities. Basically, this is the naked objects / scaffolding type system that’s long been in Zope 3 wherein an object’s specification can be used to assist in building forms and validation. And while I did have to mix in some custom code to support working in an Ajax environment instead of regular full page request / response environment, I didn’t have to do much. Zope 3’s system is pretty well structured here.
  • It incorporates a custom widget for doing ‘tags’. The ‘tags’ widget is a regular HTML textinput widget, but on the server side it is cleaned up and parsed into keywords (split on whitespace, punctuation removed).
  • When the form is submitted, AJAX is again used. Zope 3’s add form submits to itself and performs a redirect on success, or highlights validation errors in place. On a validation failure, this works out quite well with prototype.js’s AjaxRequest and AjaxUpdater objects. It was a little more tricky to deal with on a success. I’ll cover that in a minute.
  • Tags are indexed right now as simple ‘tag’ objects which find their associated posts. It’s a brute force solution, but it’s working. The reindexing occurs every time a Snippet post is added using Zope 3’s events system. When I expand the system, this will deal with modifications and deletions as well.

So far, I’m actually using very few of Zope 3’s more advanced features, but they’re there to grow into.

So how did I deal with the add form’s success? Expecting to do a redirect from within Zope didn’t quite work, because the XMLHTTPRequest system would follow the redirect and put that in the middle of the page. So I needed to return a value that I could use within Javascript to do a redirect. Again – this is only on a success. On a validation failure, Zope 3’s addform’s basic behavior is exactly what I want. So on a success, I needed Zope 3 to redirect to something that would return a URL I could redirect to. No existing object or class, aside from my modified addform view, needed to know of this new view’s existence. Zope 3’s view system made this easy, and with a couple lines of code, and a couple of lines of configuration to register the view, I have my ridiculously simple but useful view working.

On the client side, I have the following in my Javascript library. There may be a better way to do this, but I’m no Javascript/DHTML guru, except for where libraries like prototype.js have liberated me.

function respondToAddSnippet(content, container) {
  // When a snippet is added, refresh the page if the result starts with
  // http. Otherwise, place the contents of the result back into the container.
  if(content.substring(0,4) == 'http') {
    window.location = content;
  } else {
    $(container).innerHTML = content;
  }
}
function add_snippet(url, form_id, container) {
  var parameters = Form.serialize(form_id);
  parameters = parameters + '&UPDATE_SUBMIT=Add';
  new Ajax.Request(url, {
    parameters: parameters,
    onComplete: function(transport) { 
      respondToAddSnippet(transport.responseText, container);
      }
    })
  return false;
}

With “add_snippet” being bound to the add form’s onSubmit event. My custom code for the add view consists of the following:

class SnippetPostAdder:
    def add(self, content):
        """See zope.app.container.interfaces.IAdding
        """
        container = self.context
        chooser = INameChooser(container)

        # check precondition
        checkObject(container, None, content)

        name = chooser.chooseName(None, content)

        container[name] = content
        return container[name]

    def nextURL(self):
        next_url = '%s/@@index_url' % zapi.absoluteURL(
            self.context, self.request)
        return next_url

With the ”@@index_url” being the call to the custom “index url” view mentioned above. Other code that is used by the Zope add form machinery uses ‘nextURL()’ to redirect to the next url on success. It’s Zope’s add form machinery that processes the input, with a lot of options on when to set positional and keyword arguments on the factory and setting properties directly. It also fires off relevant Zope events. When done, it calls self.add(content), as shown above. This is a straight-ahead adder which generates a name (a unique id for the container) before adding the content to the container. There is Zope machinery that handles this when one goes through a slightly heavier add process, but this is all that is needed here.