Weblog

published Nov 03, 2021, last modified Nov 04, 2021

New Plone product: roulationpolicy

published Jun 04, 2007

Do you want to add a document to a folder that ends up (almost) at the top? Or do you want to roulate/rotate contents of a folder so a different one ends up on top each day or so? Zest Software presents a Plone product that does just that: roulationpolicy.

First: get the source at the Plone collective.

What the product does is best explained by the README.txt which I will add below. Look at roulation.txt for more doctests.

Usually when adding an object to a folder, this is added at the last position. This is not always wanted. You could use browser views or maybe SmartFolders to instead show the last item first, but this is not always the right solution. When you really want your new items to be added at (or near) the top, the roulationpolicy product is for you.

Adding content in a different position

This product uses a roulation policy when adding content to a folder. You can let content types implement the IRoulationContent interface. If you do not understand what I just said, ask a programmer. ;-) When such a content object is added to a folder, the item now gets added right below the top. This means, not at position zero (which is the top) but position one.

Roulating content to the bottom

A container (folder) can implement the IRoulationContainer interface. This means that every day the top most content object in that container is moved to the bottom.

Dependencies

  • Zope 2.9 (or higher). The product uses zope 3 events, which means that it likely will not work with Zope 2.8.
  • Archetypes (for the moveObjects* functions). I use the Plone 2.5 bundle containing that.
  • For testing:
    • Five: at least version 1.4 as that contains testbrowser.
    • ATConttentTypes as we will use ATDocument and ATFolder to demonstrate our product. I use the Plone 2.5 bundle containing that.

Developers

See roulation.txt as a doctest on how to use this product. But basically it is this.

Choose content types that will implement our interfaces. For example, ATDocument can be roulation content.

>>> from zope.interface import classImplements
>>> from Products.roulationpolicy.interfaces import IRoulationContent
>>> from Products.ATContentTypes.content.document import ATDocument
>>> classImplements(ATDocument, IRoulationContent)

This will make sure that new ATDocuments will be added right below the top of a folder.

Now you can let a folderish content type be a roulation container, for example ATFolder.

>>> from Products.roulationpolicy.interfaces import IRoulationContainer
>>> from Products.ATContentTypes.content.folder import ATFolder
>>> classImplements(ATFolder, IRoulationContainer)

Now add some code in a place that is locical to you that fires an event when the items in the folderish type need to be roulated:

>>> from zope.event import notify
>>> from Products.roulationpolicy.events import ContainerRoulationEvent
>>> notify(ContainerRoulationEvent(self.folder))

This will make sure that the top most item in self.folder gets moved to the bottom, making all the other items shift to the top.

Instead, you can also call a browser view that does this for you

>>> from Products.Five.testbrowser import Browser
>>> browser = Browser()
>>> browser.open(self.folder.absolute_url() + '/@@roulate_folder')

Annotations for time registration

published Jun 01, 2007, last modified Jun 16, 2007

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.

Using interfaces in a Plone Product

published Jun 01, 2007

The eXtremeManagement product has seen lots of updates recently. I am writing a report for school about that. I will put the report online as a pdf file when it is ready. But I thought it would be nice to publish some parts in advance. Feedback can help me to make a better report. The intended audience is mostly Plone (partly Zope) developers. The teachers at school who are going to give me a grade for this should also be able to follow it though and they do not know much about Zope and Plone. So this blog entry and some others like it should be fairly readable also for programmers new to Zope and Plone (and python probably).

In Zope 3, interfaces are important. Interfaces are classes that define functionality. Other classes that claim to implement that interface should provide that functionality. The simplest kind of interface is the marker interface. An example is in interfaces/xmstory.py:

from zope.interface import Interface

class IXMStory(Interface):
    """eXtremeManagement Story
    """

It is called a marker interface because you can mark a class with it. Marking means that you claim that a certain class implements this interface. We do that in content/Story.py:

from Products.eXtremeManagement.interfaces import IXMStory
...
class Story(OrderedBaseFolder):
    ...
    implements(IXMStory)

On its own this does absolutely nothing. But now we can do something with this interface in other parts of our code. For example, in browser/configure.zcml we register a certain view or page only for objects that implement this IXMStory interface:


Details aside, this now means that the page with the name story is available exclusively for objects implementing that interface, instead of all objects, which would be much less clean.

As a first step it is enough to define marker interfaces for all our content types. So in the interfaces/ directory we have defined interfaces IXMProject, IXMIteration, etcetera.

Interfaces can be much more than just markers, though. They can define functionality. Objects that implement this interface then promise to provide that functionality. In the file timing/interfaces.py we define an interface like this:

from zope.interface import Interface
from zope.interface import Attribute

class IActualHours(Interface):
    """Actual hours and minutes worked
    """

    actual_time = Attribute("Actual time")

    def recalc():
        """Recalculate the total of bookings/actual hours.
        """

Any class that claims to implement this IActualHours interface must at least have an attribute actual_time and a method recalc. This is true for our Booking class, so we can claim to implement the interface. Instead of saying this in python code, like in the example for IXMStory above, let's now say this in timing/configure.zcml:


  

We will later see how this is used in practice.

Nieuwe versie site

published May 23, 2007, last modified Jun 04, 2007

De nieuwe versie van mijn website is in de lucht!

Mijn website heeft een nogal grondige opknapbeurt gehad. De inhoud is eigenlijk niet veranderd. Maar onderhuids is de boel volledig overhoop gehaald.

Eerst draaide mijn website op het web applicatie framework Zope 2.7. Nu zit er Zope 2.9 onder en het CMS (Content Management Systeem) Plone 2.5. Twee andere belangrijke nieuwe onderdelen zijn het weblog product Quills en LinguaPlone. Quills gebruik ik voor dit weblog en voor het weblog (preeklog, podcast , preekcast) van mijn kerk. Met LinguaPlone regel ik wanneer er Engelse en wanneer Nederlandse vertalingen van de inhoud zichtbaar zijn.

Oude links zouden nog steeds moeten werken, soms via een redirect.  Zie je ergens (hier of op andere sites) verwijzingen die niet kloppen, meld dat dan bij mij. De officiële locaties van de atom feeds zijn overigens wel gewijzigd. Dus als je van de feeds gebruik maakt, pas die dan graag aan. Verkeer naar de oude feeds wordt wel correct doorgestuurd.

Veel plezier met deze vernieuwde site!

New version of site

published May 23, 2007, last modified Jun 04, 2007

The new version of my website is live.

My website has got a rather thorough refreshing. The contents have not really changed. But underneath the surface a lot has been improved.

At first my website was running on the web application framework Zope 2.7. Now I have Zope 2.9 and the CMS (Content Management System) Plone 2.5. Two other important new parts are the weblog product Quills and LinguaPlone. I am using Quills for this weblog and for the weblog (podcast) of Dutch sermons from my church. With LinguaPlone I make sure that English and Dutch translations of content are visible at the right time.

Old links should still work, possibly through redirects.  Should you find any broken links here or on other sites, please tell me. The official location of the atom feeds has changed by the way. So if you are using them, please change to the new locations (visible in the portlet when you view the weblog).

I hope you enjoy the new site!