More notes/thoughts

svn path=/plone.app.discussion/trunk/; revision=26919
This commit is contained in:
Martin Aspeli 2009-05-13 15:20:02 +00:00
parent 3b2db86d54
commit d3a9cecf6b
4 changed files with 103 additions and 57 deletions

View File

@ -4,6 +4,47 @@ plone.app.discussion design notes
This document contains design notes for plone.app.discussion. 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 ++comments++ 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/++comments++/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 ++comments++ namespace should use this). This makes it possible to replace
conversations and comments transparently.
The Comment class The Comment class
----------------- -----------------
@ -49,53 +90,12 @@ metadata such as creator, effective date etc.
Finally, we'll need event handlers to perform the actual indexing. Finally, we'll need event handlers to perform the actual indexing.
The Discussable class
---------------------
To obtain comments for a content item, we adapt the content to IDiscussable.
This has a 'replies' BTree.
To support __parent__ pointer integrity and stable URLs, we will implement
IDiscussable as a persistent object stored in an annotation. Comments directly
in reply to the content item have a __parent__ pointing to this, which in turn
has a __parent__ pointing at the content item.
The IDiscussable interface also maintains information about the total number
of comments and the unique set of commenters' usernames. These are indexed in
the catalog, allowing queries like "recently commented-upon content items",
"my comments", or "all comments by user X". These values need to be stored and
maintained by event handlers, not calculated on the fly.
See collective.discussionplus for inspiration.
Traversal and acquisition
--------------------------
A comment may have a URL such as:
http://localhost:8080/site/content/++comments++/1/2/3
For this traversal to work, we have:
- a namespace traversal adapter for ++comments++ that looks up an
IDiscussable adapter on the context and returns this
- an IPublishTraverse adapter for IHasReplies (inherited by IDiscussable
and IComment), which looks up values in the 'replies' dictionary and
acquisition-wraps them.
- the IDiscussable adapter needs to have an id of ++comment++
XXX: unrestrictedTraverse() does not take IPublishTraverse adapters into
account. This may mean we need to implement __getitem__ on comments instead
of/in addition to using a custom IPublishTraverse for IHasReplies.
Discussion settings Discussion settings
------------------- -------------------
Discussion can be enabled per-type and per-instance, via values in the FTI 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 (allow_discussion) and on the object. These will remain unchanged. The
IDiscussable object's 'enabled' property should consult these. IConversation object's 'enabled' property should consult these.
Global settings should be managed using plone.registry. A control panel Global settings should be managed using plone.registry. A control panel
can be generated from this as well, using the helper class in can be generated from this as well, using the helper class in

View File

@ -24,7 +24,8 @@ class Comment(Explicit, Traversable, RoleManager, Owned):
meta_type = portal_type = 'Discussion Item' meta_type = portal_type = 'Discussion Item'
__parent__ = None __parent__ = None
__name__ = None
comment_id = None # int
title = u"" title = u""
@ -40,8 +41,8 @@ class Comment(Explicit, Traversable, RoleManager, Owned):
author_name = None author_name = None
author_email = None author_email = None
def __init__(self, id=None, conversation=None, **kw): def __init__(self, id=0, conversation=None, **kw):
self.__name__ = unicode(id) self.comment_id = id
self.__parent__ = conversation self.__parent__ = conversation
for k, v in kw: for k, v in kw:
@ -49,9 +50,13 @@ class Comment(Explicit, Traversable, RoleManager, Owned):
# convenience functions # convenience functions
@property
def __name__(self):
return unicode(self.comment_id)
@property @property
def id(self): def id(self):
return str(self.__name__) return str(self.comment_id)
def getId(self): def getId(self):
"""The id of the comment, as a string """The id of the comment, as a string

View File

@ -1,12 +1,21 @@
<configure xmlns="http://namespaces.zope.org/zope" i18n_domain="plone.app.discussion"> <configure xmlns="http://namespaces.zope.org/zope" i18n_domain="plone.app.discussion">
<include file="permissions.zcml" /> <include file="permissions.zcml" />
<include package=".browser" />
<utility object=".comment.CommentFactory" name="Discussion Item" /> <!-- Comments -->
<class class=".comment.Comment"> <class class=".comment.Comment">
<require interface=".comment.Comment" permission="zope2.View" /> <require interface=".interfaces.IComment" permission="zope2.View" />
<require attributes="Title Creator getId" permission="zope2.View" /> <require attributes="Title Creator getId" permission="zope2.View" />
</class> </class>
<utility object=".comment.CommentFactory" name="Discussion Item" />
<!-- Conversations -->
<class class=".conversation.Conversation">
<require interface=".interfaces.IConversation" permission="zope2.View" />
</class>
<adapter factory=".conversation.conversationAdapterFactory" />
</configure> </configure>

View File

@ -10,8 +10,12 @@ manipulating the comments directly in reply to a particular comment or at the
top level of the conversation. top level of the conversation.
""" """
from zope.interface import implements from persistence import Persistent
from zope.component import adapts
from zope.interface import implements, implementer
from zope.component import adapts, adapter
from zope.annotation.interfaces import IAnnotatable
from BTrees.OIBTree import OIBTree from BTrees.OIBTree import OIBTree
from BTrees.IOBTree import IOBTree from BTrees.IOBTree import IOBTree
@ -21,8 +25,11 @@ from Acquisition import Explicit
from plone.app.discussion.interfaces import IConversation, IComment, IReplies from plone.app.discussion.interfaces import IConversation, IComment, IReplies
class Conversation(Explicit): class Conversation(Persistent, Explicit):
"""A conversation is a container for all comments on a content object. """A conversation is a container for all comments on a content object.
It manages internal data structures for comment threading and efficient
comment lookup.
""" """
implements(IConversation) implements(IConversation)
@ -80,16 +87,41 @@ class Conversation(Explicit):
# TODO: Update internal data structures when items added or removed # TODO: Update internal data structures when items added or removed
class ConversationReplies(object): @implementer(IConversation)
@adapter(IAnnotatable)
def conversationAdapterFactory(content):
"""Adapter factory to fetch a conversation from annotations
""" """
# TODO
return None
class ConversationReplies(object):
"""An IReplies adapter for conversations.
This makes it easy to work with top-level comments.
""" """
implements(IReplies) implements(IReplies)
adapts(Conversation) adapts(Conversation)
class CommentReplies(object): def __init__(self, context):
""" self.conversation = context
self.root = 0
# TODO: dict interface - generalise to work with any starting point, so
# that the subclassing below works
class CommentReplies(ConversationReplies):
"""An IReplies adapter for comments.
This makes it easy to work with replies to specific comments.
""" """
implements(IReplies) implements(IReplies)
adapts(IComment) adapts(IComment)
def __init__(self, context):
self.conversation = context.__parent__
self.root = context.comment_id