From e35f761939c298b1f1289bdfc57c207d477d190e Mon Sep 17 00:00:00 2001 From: Lennart Regebro Date: Sat, 16 May 2009 15:05:22 +0000 Subject: [PATCH] Added a central tool that indexes certain important data about comments, for usage of the commenting moderation UI. svn path=/plone.app.discussion/trunk/; revision=26972 --- plone/app/discussion/configure.zcml | 14 +++++ plone/app/discussion/conversation.py | 48 +++++++++++++-- plone/app/discussion/interfaces.py | 17 ++++++ .../profiles/default/componentregistry.xml | 9 +++ plone/app/discussion/tests/test_api.py | 4 +- plone/app/discussion/tests/test_tool.py | 52 ++++++++++++++++ plone/app/discussion/tool.py | 59 +++++++++++++++++++ 7 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 plone/app/discussion/profiles/default/componentregistry.xml create mode 100644 plone/app/discussion/tests/test_tool.py create mode 100644 plone/app/discussion/tool.py 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)