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)
|
||||
@ -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)
|
||||
adapts(IComment)
|
||||
|
||||
def __init__(self, context):
|
||||
self.conversation = context.__parent__
|
||||
self.root = context.comment_id
|
||||
|
Loading…
Reference in New Issue
Block a user