diff --git a/plone/app/discussion/configure.zcml b/plone/app/discussion/configure.zcml
index 921eba2..06e0875 100644
--- a/plone/app/discussion/configure.zcml
+++ b/plone/app/discussion/configure.zcml
@@ -32,4 +32,18 @@
+
+
+
+
+
+
diff --git a/plone/app/discussion/conversation.py b/plone/app/discussion/conversation.py
index 95534c7..6bc4437 100644
--- a/plone/app/discussion/conversation.py
+++ b/plone/app/discussion/conversation.py
@@ -14,6 +14,15 @@ from persistent import Persistent
from zope.interface import implements, implementer
from zope.component import adapts, adapter
+from zope.annotation.interfaces import IAnnotations
+
+from zope.event import notify
+from zope.app.container.interfaces import IObjectAddedEvent
+from OFS.event import ObjectWillBeAddedEvent
+from OFS.event import ObjectWillBeRemovedEvent
+from zope.app.container.contained import ContainerModifiedEvent
+from zope.app.container.contained import ObjectAddedEvent
+from zope.app.container.contained import ObjectRemovedEvent
from zope.annotation.interfaces import IAnnotatable
@@ -21,17 +30,19 @@ from BTrees.OIBTree import OIBTree
from BTrees.IOBTree import IOBTree
from BTrees.IIBTree import IIBTree, IISet
try:
+ # These exist in new versions, but not in the one that comes with Zope 2.10.
from BTrees.LOBTree import LOBTree
- from BTrees.LLBTree import LLBTree # TODO: Does this even exist?
+ from BTrees.LLBTree import LLSet
except ImportError:
from BTrees.OOBTree import OOBTree as LOBTree
from BTrees.OOBTree import OOSet as LLSet
from Acquisition import Explicit
-
from plone.app.discussion.interfaces import IConversation, IComment, IReplies
+ANNO_KEY = 'plone.app.discussion:conversation'
+
class Conversation(Persistent, Explicit):
"""A conversation is a container for all comments on a content object.
@@ -88,7 +99,8 @@ class Conversation(Persistent, Explicit):
def addComment(self, comment):
id = comment.comment_id
if id in self._comments:
- id = max(self._comments.keys()) +1
+ id = max(self._comments.keys()) + 1
+ notify(ObjectWillBeAddedEvent(comment, self, id))
self._comments[id] = comment
comment.comment_id = id
@@ -103,9 +115,28 @@ class Conversation(Persistent, Explicit):
if not reply_to in self._children:
self._children[reply_to] = LLSet()
self._children[reply_to].insert(id)
+ notify(ObjectAddedEvent(comment, self, id))
+ notify(ContainerModifiedEvent(self))
# Dict API
+ def __getitem__(self, key):
+ return self._comments[key]
+
+ def __setitem__(self, key, value):
+ # XXX Check that it implements the commenting interface
+ if value.comment_id in self._comments:
+ raise ValueError("Can not replace an existing comment")
+ # Note that we ignore the key completely:
+ self.addComment(comment)
+
+ def __delitem__(self, key):
+ # TODO unindex everything
+ return self._comments.remove(key)
+
+ def keys(self):
+ return self._comments.keys()
+
# TODO: Update internal data structures when items added or removed
@implementer(IConversation)
@@ -113,9 +144,14 @@ class Conversation(Persistent, Explicit):
def conversationAdapterFactory(content):
"""Adapter factory to fetch a conversation from annotations
"""
-
- # TODO
- return None
+ annotions = IAnnotations(content)
+ if not ANNO_KEY in annotions:
+ conversation = Conversation()
+ conversation._parent_uid = content.UID()
+ annotions[ANNO_KEY] = conversation
+ conversation = annotions[ANNO_KEY]
+ # Probably this needs an acquisition wrapper
+ return conversation
class ConversationReplies(object):
"""An IReplies adapter for conversations.
diff --git a/plone/app/discussion/interfaces.py b/plone/app/discussion/interfaces.py
index 21e8343..3009f91 100644
--- a/plone/app/discussion/interfaces.py
+++ b/plone/app/discussion/interfaces.py
@@ -116,3 +116,20 @@ class IComment(Interface):
# for anonymous comments only, set to None for logged in comments
author_name = schema.TextLine(title=_(u"Author name"), required=False)
author_email = schema.TextLine(title=_(u"Author email address"), required=False)
+
+class ICommentingTool(Interface):
+ """A tool that indexes all comments for usage by the management interface.
+
+ This was the management interface can still work even though we don't
+ index the comments in portal_catalog.
+ """
+
+ def index(comment):
+ """Indexes a comment"""
+
+ def unindex(comment):
+ """Removes a comment from the indexes"""
+
+ def search(username=None, wfstate=None):
+ """Get all comments with a certain username of wfstate"""
+
\ No newline at end of file
diff --git a/plone/app/discussion/profiles/default/componentregistry.xml b/plone/app/discussion/profiles/default/componentregistry.xml
new file mode 100644
index 0000000..9764a7f
--- /dev/null
+++ b/plone/app/discussion/profiles/default/componentregistry.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/plone/app/discussion/tests/test_api.py b/plone/app/discussion/tests/test_api.py
index 80ee97b..96d552b 100644
--- a/plone/app/discussion/tests/test_api.py
+++ b/plone/app/discussion/tests/test_api.py
@@ -15,6 +15,7 @@ from Products.PloneTestCase.layer import PloneSite
from plone.app.discussion.conversation import Conversation
from plone.app.discussion.comment import Comment
+from plone.app.discussion.interfaces import ICommentingTool, IConversation
class ConversationTest(TestCase):
def afterSetUp(self):
@@ -27,7 +28,7 @@ class ConversationTest(TestCase):
def test_add_comment(self):
# Create a conversation. In this case we doesn't assign it to an
# object, as we just want to check the Conversation object API.
- conversation = Conversation()
+ conversation = IConversation(self.portal.doc1)
# Add a comment. reply_to=0 means it's not a reply
comment = Comment(conversation=conversation, reply_to=0)
@@ -43,7 +44,6 @@ class ConversationTest(TestCase):
self.assertEquals(conversation.total_comments, 1)
self.assert_(conversation.last_comment_date - datetime.now() < timedelta(seconds=1))
-
def test_suite():
return unittest.TestSuite([
unittest.makeSuite(ConversationTest),
diff --git a/plone/app/discussion/tests/test_tool.py b/plone/app/discussion/tests/test_tool.py
new file mode 100644
index 0000000..730e143
--- /dev/null
+++ b/plone/app/discussion/tests/test_tool.py
@@ -0,0 +1,52 @@
+import unittest
+from datetime import datetime, timedelta
+from base import TestCase
+
+from zope.testing import doctestunit
+from zope.component import testing, getMultiAdapter, getUtility
+from zope.publisher.browser import TestRequest
+from zope.publisher.interfaces.browser import IBrowserView
+from Testing import ZopeTestCase as ztc
+
+from Products.Five import zcml
+from Products.Five import fiveconfigure
+from Products.PloneTestCase import PloneTestCase as ptc
+from Products.PloneTestCase.layer import PloneSite
+
+from plone.app.discussion.conversation import Conversation
+from plone.app.discussion.comment import Comment
+from plone.app.discussion.interfaces import ICommentingTool, IConversation
+
+class ToolTest(TestCase):
+ def afterSetUp(self):
+ # XXX If we make this a layer, it only get run once...
+ # First we need to create some content.
+ self.loginAsPortalOwner()
+ typetool = self.portal.portal_types
+ typetool.constructContent('Document', self.portal, 'doc1')
+
+ def test_tool_indexing(self):
+ # Create a conversation. In this case we doesn't assign it to an
+ # object, as we just want to check the Conversation object API.
+ conversation = IConversation(self.portal.doc1)
+
+ # Add a comment. reply_to=0 means it's not a reply
+ comment = Comment(conversation=conversation, reply_to=0)
+ comment.title = 'Comment 1'
+ comment.text = 'Comment text'
+
+ conversation.addComment(comment)
+
+ # Check that the comment got indexed in the tool:
+ tool = getUtility(ICommentingTool)
+ comment = list(tool.search())[0]
+ self.assertEquals(comment['text'], 'Comment text')
+
+
+def test_suite():
+ return unittest.TestSuite([
+ unittest.makeSuite(ToolTest),
+ ])
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
diff --git a/plone/app/discussion/tool.py b/plone/app/discussion/tool.py
new file mode 100644
index 0000000..1c90d1b
--- /dev/null
+++ b/plone/app/discussion/tool.py
@@ -0,0 +1,59 @@
+import time
+from zope import interface
+from zope.component import getUtility
+
+from BTrees.OOBTree import OOBTree, OOSet, intersection
+
+from interfaces import ICommentingTool
+# The commenting tool, which is a local utility
+
+class CommentingTool(object):
+ interface.implements(ICommentingTool)
+
+ def __init__(self):
+ self._id2uid = OOBTree() # The comment ID to object UID
+ self._id2text = OOBTree() # The text for a comment
+ self._wfstate2id = OOBTree() # To search on wf states
+ self._creator2id = OOBTree() # To search/order on creator ids
+
+ def index(self, comment):
+ # Store the object in the store:
+ id = comment.comment_id
+ self._id2uid[id] = comment.__parent__._parent_uid
+ self._id2text[id] = comment.text
+
+ # TODO
+ ## Index on workflow state
+ #wfstate = comment.getWorkflowState()
+ #if not wfstate in self._wfstate2id:
+ #self._wfstate2id[wfstate] = OOSet()
+ #self._wfstate2id[wfstate].insert(id)
+
+ # Index on creator
+ creator = comment.creator
+ if not creator in self._creator2id:
+ self._creator2id[creator] = OOSet()
+ self._creator2id[creator].insert(id)
+
+ def search(self, creator=None):
+ if creator is not None:
+ # Get all replies for a certain object
+ ids = self._creator2ids.get(creator, None)
+ if ids is None:
+ raise StopIteration
+ else:
+ ids = self._id2uid.keys()
+
+ for id in ids:
+ yield {'id': id,
+ 'text': self._id2text[id]
+ # TODO: More data + maybe brains or something?
+ }
+
+def object_added_handler(obj, event):
+ tool = getUtility(ICommentingTool)
+ tool.index(obj)
+
+def object_removed_handler(obj, event):
+ tool = getUtility(ICommentingTool)
+ tool.unindex(obj)