diff --git a/plone/app/discussion/NOTES.txt b/plone/app/discussion/NOTES.txt index 603f42a..8b27084 100644 --- a/plone/app/discussion/NOTES.txt +++ b/plone/app/discussion/NOTES.txt @@ -4,6 +4,47 @@ plone.app.discussion 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 ++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 ----------------- @@ -49,53 +90,12 @@ metadata such as creator, effective date etc. 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 can be enabled per-type and per-instance, via values in the FTI (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 can be generated from this as well, using the helper class in diff --git a/plone/app/discussion/comment.py b/plone/app/discussion/comment.py index 9299797..8322fcf 100644 --- a/plone/app/discussion/comment.py +++ b/plone/app/discussion/comment.py @@ -24,7 +24,8 @@ class Comment(Explicit, Traversable, RoleManager, Owned): meta_type = portal_type = 'Discussion Item' __parent__ = None - __name__ = None + + comment_id = None # int title = u"" @@ -40,8 +41,8 @@ class Comment(Explicit, Traversable, RoleManager, Owned): author_name = None author_email = None - def __init__(self, id=None, conversation=None, **kw): - self.__name__ = unicode(id) + def __init__(self, id=0, conversation=None, **kw): + self.comment_id = id self.__parent__ = conversation for k, v in kw: @@ -49,9 +50,13 @@ class Comment(Explicit, Traversable, RoleManager, Owned): # convenience functions + @property + def __name__(self): + return unicode(self.comment_id) + @property def id(self): - return str(self.__name__) + return str(self.comment_id) def getId(self): """The id of the comment, as a string diff --git a/plone/app/discussion/configure.zcml b/plone/app/discussion/configure.zcml index eaa3de7..97c29a6 100644 --- a/plone/app/discussion/configure.zcml +++ b/plone/app/discussion/configure.zcml @@ -1,12 +1,21 @@ + - - + - + + + + + + + + + + diff --git a/plone/app/discussion/conversation.py b/plone/app/discussion/conversation.py index a5051b8..9bb1842 100644 --- a/plone/app/discussion/conversation.py +++ b/plone/app/discussion/conversation.py @@ -10,8 +10,12 @@ manipulating the comments directly in reply to a particular comment or at the top level of the conversation. """ -from zope.interface import implements -from zope.component import adapts +from persistence import Persistent + +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.IOBTree import IOBTree @@ -21,8 +25,11 @@ from Acquisition import Explicit 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. + + It manages internal data structures for comment threading and efficient + comment lookup. """ implements(IConversation) @@ -79,17 +86,42 @@ class Conversation(Explicit): # Dict API # 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) adapts(Conversation) + + 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(object): - """ +class CommentReplies(ConversationReplies): + """An IReplies adapter for comments. + + This makes it easy to work with replies to specific comments. """ implements(IReplies) - adapts(IComment) \ No newline at end of file + adapts(IComment) + + def __init__(self, context): + self.conversation = context.__parent__ + self.root = context.comment_id + \ No newline at end of file