Design Notes

This document contains design notes for plone.app.discussion.

Storage and traversal

For each content item, there is a Conversation object stored in annotations. This can be traversed to via the ++conversation++ namespace, but also fetched via an adapter lookup to IConversation.

The conversation stores all comments related to a content object. Each comment has an integer id (also representable as a string, to act as an OFS id and allow traversal). Hence, traversing to obj/++conversation++/123 retrieves the comment with id 123.

Comments ids are assigned in order, so a comment with id N was posted before a comment with id N + 1. However, it is not guaranteed that ids will be incremental. Ids must be positive integers - 0 or negative numbers are not allowed.

Threading information is stored in the conversation: we keep track of the set of children and the parent if any comment. Top-level comments have a parent id of 0. This information is managed by the conversation class when comments are manipulated using a dict-like API.

Note that the __parent__/acquisition parent of an IComment is the IConversation, and the __parent__/acquisition parent of an IConversation is the content object.

Events

Manipulating the IConversation object should fire the usual IObjectAddedEvent and IObjectRemovedEvent events. The UI may further fire IObjectCreatedEvent and IObjectModifiedEvent for comments.

Factories

Comments should always be created via the ‘Discussion Item’ IFactory utility. Conversations should always be obtained via the IConversation adapter (even the ++conversation++ namespace should use this). This makes it possible to replace conversations and comments transparently.

The Comment class

The inheritance tree for DiscussionItem is shown below. Classes we want to mix in and interface we want to implement in the Comment class are marked with [x].

[ ] DiscussionItem
    [ ] Document
        [ ] PortalContent                       = [ ] IContentish
            [ ] DynamicType                     = [ ] IDynamicType
            [ ] CMFCatalogAware                 = [ ] <no interface>
            [ ] SimpleItem                      = [ ] ISimpleItem
                [ ] Item                          [ ]
                    [?] Base                    = [ ] <no interface>
                    [ ] Resource                = [ ] <no interface>
                    [ ] CopySource              = [ ] ICopySource
                    [ ] Tabs                    = [ ] <no interface>
                    [x] Traversable             = [ ] ITraversable
                    [ ] Element                 = [ ] <no interface>
                    [x] Owned                   = [ ] IOwned
                    [ ] UndoSupport             = [ ] IUndoSupport
                [ ] Persistent                    [ ]
                [ ] Implicit                      [ ]
                [x] RoleManager                 = [ ] IRoleManager
                    [ ] RoleManager             = [ ] IPermissionMappingSupport
        [ ] DefaultDublinCoreImpl               = [ ] IDublinCore
                                                  [ ] ICatalogableDublinCore
                                                  [ ] IMutableDublinCore
            [ ] PropertyManager                 = [ ] IPropertyManager

Thus, we want:

  • Traversable, to get absolute_url() and friends
    • this requires a good acquisition chain at all times
  • Acquisition.Explicit, to support acquisition
    • we do not want implicit acquisition
  • Owned, to be able to track ownership

  • RoleManager, to support permissions and local roles

We also want to use a number of custom indexers for most of the standard metadata such as creator, effective date etc.

Finally, we’ll need event handlers to perform the actual indexing.

Discussion settings

Discussion can be enabled per-type and per-instance, via values in the FTI (allow_discussion) and on the object. These will remain unchanged. The IConversation object’s ‘enabled’ property should consult these.

Global settings should be managed using plone.registry. A control panel can be generated from this as well, using the helper class in plone.app.registry.

Note that some settings, notably those to do with permissions and workflow, will need to be wired up as custom form fields with custom data mangers or similar.

Workflow and permissions

Where possible, we should use existing permissions:

  • View
  • Reply to Item
  • Modify Portal Content
  • Request Review

In addition, we’ll need a ‘Moderator’ role and a moderation permission,

  • Moderate comment
  • Bypass moderation

To control whether Anonymous can post comments, we manage the ‘Reply to Item’ permission. To control whether moderation is required for various roles, we could manage the ‘Bypass moderation’ permission.

These could work in a workflow like this:

* --> [posted] -- {publish} --> [published]--> *
         |                          ^
         |                          |
         +----- {auto-publish} -----+
         |                          |
         +----- {auto-moderate} ----+

The ‘posted’ state is the initial state. ‘published’ is the state where the comment is visible to non-reviewers.

The ‘publish’ transition would be protected by the ‘Moderate comment’ permission. We could have states and transition for ‘rejected’, etc, but it is probably just as good to delete comments that are rejected.

The ‘auto-publish’ transition would be an automatic transition protected by the ‘Bypass moderation’ permission.

The ‘auto-moderate’ transition would be another automatic transition protected by an expression (e.g. calling a view) that returns True if the user is on an auto-moderation ‘white-list’, e.g. by email address or username.

Forms and UI

The basic commenting display/reply form is placed in a viewlet.

The reply form is dynamically created right under the comment when the user hits the reply button. To do so, we copy the standard comment form with a jQuery function. This function sets the form’s hidden in_reply_to field to the id of the comment the user wants to reply to. This also makes it possible to use z3c.form validation for the reply forms, because we can uniquely identify the reply form request and return the reply form with validation errors.

Since we rely on JavaScript for the reply form creation, the reply button is removed for non JavaScript enabled browsers.

The comment form uses z3c.form and plone.z3cform’s ExtensibleForm support. This makes it possible to plug in additional fields declaratively, e.g. to include SPAM protection.

Table Of Contents

Previous topic

Architectural Principles

Next topic

Permissions and Workflows

This Page