Commenting in Plone
And a new product: collective.discussionplus
It is generally accepted that Plone's support for commenting is not quite as strong as we'd like it to be. There are several add-on products out there to try to rectify this. I've used qPloneComments and iqpp.plone.commenting before, and whilst they are both good solutions (I ended up using the latter for a big deployment), I also feel a bit uncomfortable with them. They are basically wholesale replacements, with a new API, that do not use the existing services we have in Plone.
I think long-term, Plone is due a new commenting solution. This would be a great Summer of Code project, for example. I'm going to outline some of the things I would like to see in such a solution below.
In the meantime, for the immediate needs of another project, I've created a very small product called collective.discussionplus. This surgically adds a few features to Plone's existing commenting solution: optional comment moderation with a fast way to moderate a large number of comments; the ability to construct Collection searches for "my comments" (using the "current author" criterion); and catalogue metadata applied to standard content objects to show the total number of (published) comments on the object. Note that it requires Plone 3.3 (mainly because it uses plone.indexer).
collective.discussionplus works with the framework we already have. It still uses CMFDefault's Discussion Item and Discussion Container ("talkback") types. Comment moderation is achieved using the venerable portal_workflow tool. Indexing of the number of comments on a content object is managed using container events (comment added/removed) and custom indexers.
A better commenting solution
Of course, Discussion Item is a bit long in the tooth. The API is split between the portal_discussion tool, the Discussion Item type and the opaque (literally) Discussion Container type, and can be cumbersome to use. Furthermore, Discussion Items basically sub-class CMFDefault's Document type which means they carry a lot of baggage that is simply unnecessary. Since comments are normally handled as objects, rather than through catalogue queries, this is not ideal.
So, what would a better solution look like? In the past, I've said that comments should not be content items. I still think this is true, in that it's not appropriate to build comments from Archetypes of CMF types (or Dexterity types, for that matter). However, I've come to realise that in many ways, comments are content. They still need to be indexed, searchable, and so on.
Let's summarise some of the use cases that I think are important:
- Comments should be subject to workflow. The most common workflows will be a one-state always-published workflow, or a two-state pending-published type moderation.
- Comments must have permissions like any proper Zope 2 object. Important permissions include 'View', 'Modify portal content' and 'Can reply'. These should of course be managed via workflow.
- Comments should be searchable using the normal portal_catalog tool.
- Each comment must have a unique, publishable URL and path.
- When viewing a comment, it should always be shown in its context. We achieve this by redirecting to the parent content object in the default view of a comment.
- Comments should be threaded. It must be possible to obtain both a comment's parent and its ultimate parent (the content item that is being discussed) easily. At the same time, it should be possible to display comments in a flat list, possibly based on a per-type configuration setting.
- Comments need to carry basic metadata, such as the author of the comment, a title, creation date and modification date. This metadata should of course be indexed in the catalogue.
- Comments should contain either plain text with whitespace preserved and clickable links (the 'intelligent plain text' transform in Plone), or simple, stripped HTML with a stripped down visual editor.
- Comments should be available to anonymous users if the required permission has been granted to the Anonymous role. This should require further details, such as a name and email address, and should optionally involve Captcha verification.
- It should be possible to present a view of all discussions taking place across the portal. One way to do this, would be to show a forum-like view where content items are the discussion topic, with comments underneath and in-place reply forms.
- Content items should know how many comments they have, when they were last commented upon, and who has commented on them.
So how would we achieve this? It would take some more design work, but I think the outlines are clear.
First, we'd need a Comment object. This should mix in enough of the basic Zope 2 services to be traversable, permission-aware and so on, but no more. It should support a dict-like API to get and set child comments, thereby supporting threading. Note that this is the API only - not necessarily the storage (see below).
Comments must have a Factory Type Information in portal_types. Creating a comment should involve invoking a factory, which should set the portal_type attribute as appropriate. This will allow multiple comment implementations, and makes it possible to assign a workflow to the comment type, among other things. Comments also need to be catalogue and workflow aware.
The container does not need explicit ordering, but should return items in creation-date order when iterated over. It should fire the standard container events (ObjectAddedEvent, ObjectRemovedEvent) when children are added or removed.
Comments themselves should be stored in a Conversation object. There would generally be one of these per commentable content item, probably stored in an annotation and obtainable by adapting a content item to IConversation.
The Conversation is a BTree container. It should store all Comments in a single BTree, with creation dates as keys so that iteration naturally returns comments in date order. To facilitate threading, there should be a bi-directional relationship (either using zc.relation and z3c.relationfield or a combination of __parent__ pointers and persistent mappings) defined between a comment and its parent. To find all top-level Comments, simply find all Comments where the parent relation points to the Conversation object. To find children of each of those, look for relations where they themselves are the parent, and so on. Two additional relations will be needed: all comments need to know their containing conversation, and the conversation needs to know its parent content object.
Each Comment will have a URL and be traversable, so that restrictedTraverse(), URL publishing and TALES path expressions work. The URL will likely include a traversal namespace (++comments++) that will look up the Conversation and continue traversal from there.
When a Comment is added, a handler for the IObjectAddedEvent will ensure that it is indexed in the catalog and that the portal_workflow tool has a chance to initialise it. Here, it is important that the object is indexed with the correct URL and path, including the ++comments++ traversal namespace. Event handlers for IObjectRemovedEvent, IObjectMovedEvent and IObjectModifiedEvent would take care of un-indexing and re-indexing as appropriate.
Finally, when comments are added, removed or transitioned, event handlers should take care of updating metadata on the content item under discussion, including a tally of comments, a list of the unique commenters (to make it easy to search for "my comments" or comments of a particular user) and a timestamp of when the item was most recently commented upon.
I don't think it would be too hard to get the storage and API right. In fact, the existing discussion support with collective.discussionplus is not a million miles away, although they are still too clunky to really be extensible and re-usable.
There's obviously a lot of template/UI work as well, but it should be possible to adapt some of the existing templates as a starting point. It would be good to move the forms to using z3c.form or similar, though, as the current CMFFormController chains are cumbersome to customise. Getting things like portal-wide discussion, portal-wide comment moderation and anonymous submission right UI-wise may take a bit of experimentation, but should not be too difficult.