Using lovely.tag in an edit form (or advanced usage for zope.formlib's EditForm)

lovely.tag is a nice package that has almost all the functionality one can desire from a tagging engine. Recently I needed to integrate it with some of my content types (but not exactly as a tagging widget, but as a global registry for a global vocabulary), so I've created a couple of new widgets (an AJAX based multi-autocomplete one, based on z3c.widget.autocomplete and an add/remove widget, based on Plone's 3rd party AddRemoveWidget). I'll describe here the technique used to integrate the tagging editing in an edit form by using adapters (although this technique is described also in the form.txt from zope.formlib)

The formlib EditForm class creates a dictionary of adapters that are used for each interface to look up the fields (as attributes to the adapter objects). This translates into the following code (IActivity is a content type item):

class IActivity(Interface):
title = TextLine(title=u"Title")

class IItemWithTags(Interface):
tags = List(title=u"Tags", value_type=TextLine(title="Tag"))

class Activity(Persistent):
implements(IActivity)
title = u""

class ActivityEditForm(EditForm):
form_fields = form.Fields(IActivity) + form.Fields(IItemWithTags)
form_fields['tags'].custom_widget = MultiAutoCompleteWidget #custom built widget

Because the IItemWithTags interface is not directly offered by IActivity, there needs to be an adapter that knows how to deal with an Activity in relation with tags.

class ActivityTagsEditor(object):
adapts(IActivityItem)
implements(IActivityTags)
uid = None #the principal id. It is set externally

def __init__(self, context):
self.context = context

intids = getUtility(IIntIds)
self.tag_engine = getUtility(ITaggingEngine)
self.oid = intids.getId(self.context) #the object id

def _setTags(self, value):
self.tag_engine.update(self.oid, self.uid, value)

def _getTags(self):
return self.tag_engine.getTags(items=[self.oid])

tags = property(_getTags, _setTags)

Because of the way the tagging engine works, the adapter needs to know a principal id for whom to set those tags. So we need to overwrite the setUpWidgets method of the form class to set this id on the adapter object (see the setUpEditWidget code to see how the self.adapters registry is filled in).

    def setUpWidgets(self, ignore_request=False):
        self.adapters = {}
        self.widgets = setUpEditWidgets(
            self.form_fields, self.prefix, self.context, self.request,
            adapters=self.adapters, ignore_request=ignore_request
            )
        self.adapters[IActivityTags].uid = self.request.principal.id

Another way to set up the adapter (if you don't want to mess with setUpWidgets) is something like this in the submit handler, right before the applyChanges call:

self.adapters = {IItemsWithTags:getAdapter(self.context, IActivityWithTags)}
self.adapters[IItemsWithTags].uid = self.request.principal.id
form.applyChanges(self.context, self.form_fields, data, adapters=self.adapters)

Reading zope.formlib.form.applyChanges is very educational in order to understand how formlib applies changes to objects and how it builds the self.adapters mapping.

And finally, the adapter is simply registered with:

<zope:adapter factory=".adapters.ActivitityTagsEditor" />

Comments