Annotations for time registration
eXtremeManagement has gotten an overhaul under the surface. To keep track of actual hours booked and a total of estimates for an Iteration or Story or Task we were using ComputedFields and some ugly methods. We got rid of those. Instead we are now using clean zope 3 style annotations. Why, you ask?
Reasons
This way we can get rid of several kludgey methods on our content types, like manage_afterAdd and reindexObject. Using events, like already started on trunk, also helps here.
We get rid of ComputedFields like getRawActualHours. On a Booking this was okay: it just changed 1 hours plus 30 minutes into 1.5 hours; this is still done actually, but with a small twist. But on a Task this would calculate the total of getRawActualHours of all its children, either by waking up these objects or by querying the catalog. Instead, with annotations and events we now make sure that this total is only calculated when a Booking has been added or changed or moved.
During a reinstall GenericSetup sees that we have some indexes in our catalog.xml so it unhelpfully empties those indexes. So we need to reindex them during our reinstall. This always seemed to have a negative effect on the consistency of booked hours and estimates in the catalog. At least it seemed to help to manually recatalog first the Bookings, then the Tasks and then the Stories and possibly the Iterations after a reinstall.
GenericSetup only empties the indexes though and not the metadata columns, so this should not have been necessary. Maybe I was misled through coincidences into believing that this helped. Anyway, now everything is stored on the object itself or in annotations, so the order in which you recatalog the content types really does not matter anymore. :)
Our content types are simpler now.
- A Task does not need to be aware of any changes to its Bookings and it does not need to inform its parent Story that its estimate has changed. Keeping everything updated is now the responsibility of the zope 3 events.
- Information about children (total hours booked or estimated) is now stored separately in annotations.
In general, this approach makes me feel sane again. Plus it allows me to play with annotations and they are cool. ;-)
Implementation
The annotations are taken care of in the timing/ directory.
A few interfaces have been introduced there:
- IActualHours: this is for content types that wish to store actual hours. Bookings actually implement this functionality already, so when you adapt a Booking to the IActualHours interface the Booking itself is returned.
- IActualHoursContainer: This is a marker interface that says: this object has children with actual hours. Iterations, Stories and (Poi)Tasks are marked with this. Actually, marker interface may be a bad choice of words as it does have one method that needs to be provided: contentValues. Since the types mentioned are folderish types they provide this method already. Objects providing this interface should also be adaptable to IActualHours.
- IEstimate: this is for content types that wish to store estimates. (Poi)Tasks actually implement this functionality already, so when you adapt a Task to the IEstimate interface the Task itself is returned.
- IEstimateContainer: This is a marker interface that says: this object has children with estimates. Iterations and Stories are marked with this. The same remarks as with IActualHoursContainer can be made. Objects providing this interface should also be adaptable to IEstimate.
By adapting an object to IActualHours or IEstimate you get an object that has a recalc method that recalculates its actual hours or the estimate and stores that in an attribute. For the Containers this is stored in annotations.
For Booking this is stored in a property that functions as a computed field. The recalc method then only needs to reindex the object:
@property def actual_time(self): return self.getHours() + (self.getMinutes() / 60.0) security.declarePublic('recalc') def recalc(self): self.reindexObject(idxs=['actual_time'])
The same is done for the estimate of Tasks.
For the various Containers the recalc method fetches its children. It does not matter if they are Bookings, Tasks or Stories. It then adapts them to the IActualHours or IEstimate interface and adds the value of the stored property to its own total. For IActualHoursContainers it looks like this:
def recalc(self): """Recalculate the total booked hours for this container. """ context = self.context total = 0.0 for obj in context.contentValues(): actual = IActualHours(obj, None) if actual is not None: total += actual.actual_time self.actual_time = total context.reindexObject(idxs=['actual_time'])
Zope 3 events are used to arrange that everything is updated at the right time, namely when a relevant object is added, moved, removed or changed. Event handlers make sure that the value for the object itself is recalculated and that its parents, grandparent, etcetera are also recalculated. This uses the following code, where adapter is either IActualHours or IEstimate:
def recursivelyRecalc(object, adapter): current = aq_inner(object) anno = adapter(current, None) while anno is not None: anno.recalc() current = aq_parent(current) anno = adapter(current, None)
This value is also put in the catalog:
from Products.CMFPlone import CatalogTool as catalogtool def actual(object, portal, **kw): anno = IActualHours(object, None) if anno is not None: return anno.actual_time return None catalogtool.registerIndexableAttribute('actual_time', actual)
Extra cleanup
While I was busy, I did some related cleanup:
- Indexes and metadata for the catalog are no longer registered in the code for the content types themselves (so the content/ directory). This is now in the GenericSetup profile in catalog.xml. The former code might not even work anymore in newer versions of Plone or Archetypes, if I am informed correctly.
- In the course of time we registered far too many indexes and metadata for our content types. Most indexes were unneeded and have been removed, except getAssignees and getBookingDate. By reinstalling or applying the GenericSetup profile the others will be removed if you use Plone 2.5.3 (GenericSetup 1.2). Removing metadata columns from the catalog is not automatically possibly currently with GenericSetup. I submitted a patch [1] for that. But the following are unused and you can manually remove them: getRawActualHours, getRawDifference, getRawEstimate. Two of those have simply gotten a different name though: actual_time and estimate. The new names feel more pythonic. :) It seemed logical to change the names while I was at it.
- When you migrate the schema of Tasks then under the hood all your Tasks are created fresh. This means that lots of emails will be sent out to inform people that some tasks have been assigned to them. This is bad and I apologize for the times in the past when this went wrong while I was testing and I ended up spamming my colleagues... :) Our reinstall actually knows this and works around it by setting a boolean value that tells Tasks to not send those emails. This functionality has been there for a while, but you can now also flip this switch manually: in the portal_properties tool there is a property sheet xm_properties with a setting send_task_mails that controls this.
[1] | That patch was recently accepted into GenericSetup trunk. See issue 483 in the Zope collector. |