More notes/thoughts
svn path=/plone.app.discussion/trunk/; revision=26919
This commit is contained in:
		
							parent
							
								
									3b2db86d54
								
							
						
					
					
						commit
						d3a9cecf6b
					
				@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,21 @@
 | 
			
		||||
<configure xmlns="http://namespaces.zope.org/zope" i18n_domain="plone.app.discussion">
 | 
			
		||||
 | 
			
		||||
    <include file="permissions.zcml" />
 | 
			
		||||
    <include package=".browser" />
 | 
			
		||||
 | 
			
		||||
    <utility object=".comment.CommentFactory" name="Discussion Item" />
 | 
			
		||||
 | 
			
		||||
    <!-- Comments -->
 | 
			
		||||
    <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" />
 | 
			
		||||
    </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>
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
@ -80,16 +87,41 @@ class Conversation(Explicit):
 | 
			
		||||
    
 | 
			
		||||
    # 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)
 | 
			
		||||
    
 | 
			
		||||
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)
 | 
			
		||||
    adapts(IComment)
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, context):
 | 
			
		||||
        self.conversation = context.__parent__
 | 
			
		||||
        self.root = context.comment_id
 | 
			
		||||
    
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user