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.
 | 
					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
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
@ -79,17 +86,42 @@ class Conversation(Explicit):
 | 
				
			|||||||
    # Dict API
 | 
					    # Dict API
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # 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)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    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)
 | 
					    implements(IReplies)
 | 
				
			||||||
    adapts(IComment)
 | 
					    adapts(IComment)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def __init__(self, context):
 | 
				
			||||||
 | 
					        self.conversation = context.__parent__
 | 
				
			||||||
 | 
					        self.root = context.comment_id
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user