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…
Reference in New Issue
Block a user