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
This commit is contained in:
Lennart Regebro 2009-05-16 15:05:22 +00:00
parent a2542ea5a8
commit e35f761939
7 changed files with 195 additions and 8 deletions

View File

@ -32,4 +32,18 @@
<adapter factory=".conversation.conversationAdapterFactory" /> <adapter factory=".conversation.conversationAdapterFactory" />
<!-- Event subscribers -->
<subscriber
for="plone.app.discussion.interfaces.IComment
zope.app.container.contained.IObjectAddedEvent"
handler=".tool.object_added_handler"
/>
<subscriber
for="plone.app.discussion.interfaces.IComment
zope.app.container.contained.IObjectRemovedEvent"
handler=".tool.object_removed_handler"
/>
</configure> </configure>

View File

@ -14,6 +14,15 @@ from persistent import Persistent
from zope.interface import implements, implementer from zope.interface import implements, implementer
from zope.component import adapts, adapter 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 from zope.annotation.interfaces import IAnnotatable
@ -21,17 +30,19 @@ from BTrees.OIBTree import OIBTree
from BTrees.IOBTree import IOBTree from BTrees.IOBTree import IOBTree
from BTrees.IIBTree import IIBTree, IISet from BTrees.IIBTree import IIBTree, IISet
try: try:
# These exist in new versions, but not in the one that comes with Zope 2.10.
from BTrees.LOBTree import LOBTree from BTrees.LOBTree import LOBTree
from BTrees.LLBTree import LLBTree # TODO: Does this even exist? from BTrees.LLBTree import LLSet
except ImportError: except ImportError:
from BTrees.OOBTree import OOBTree as LOBTree from BTrees.OOBTree import OOBTree as LOBTree
from BTrees.OOBTree import OOSet as LLSet from BTrees.OOBTree import OOSet as LLSet
from Acquisition import Explicit from Acquisition import Explicit
from plone.app.discussion.interfaces import IConversation, IComment, IReplies from plone.app.discussion.interfaces import IConversation, IComment, IReplies
ANNO_KEY = 'plone.app.discussion:conversation'
class Conversation(Persistent, 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.
@ -88,7 +99,8 @@ class Conversation(Persistent, Explicit):
def addComment(self, comment): def addComment(self, comment):
id = comment.comment_id id = comment.comment_id
if id in self._comments: 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 self._comments[id] = comment
comment.comment_id = id comment.comment_id = id
@ -103,9 +115,28 @@ class Conversation(Persistent, Explicit):
if not reply_to in self._children: if not reply_to in self._children:
self._children[reply_to] = LLSet() self._children[reply_to] = LLSet()
self._children[reply_to].insert(id) self._children[reply_to].insert(id)
notify(ObjectAddedEvent(comment, self, id))
notify(ContainerModifiedEvent(self))
# Dict API # 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 # TODO: Update internal data structures when items added or removed
@implementer(IConversation) @implementer(IConversation)
@ -113,9 +144,14 @@ class Conversation(Persistent, Explicit):
def conversationAdapterFactory(content): def conversationAdapterFactory(content):
"""Adapter factory to fetch a conversation from annotations """Adapter factory to fetch a conversation from annotations
""" """
annotions = IAnnotations(content)
# TODO if not ANNO_KEY in annotions:
return None 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): class ConversationReplies(object):
"""An IReplies adapter for conversations. """An IReplies adapter for conversations.

View File

@ -116,3 +116,20 @@ class IComment(Interface):
# for anonymous comments only, set to None for logged in comments # for anonymous comments only, set to None for logged in comments
author_name = schema.TextLine(title=_(u"Author name"), required=False) author_name = schema.TextLine(title=_(u"Author name"), required=False)
author_email = schema.TextLine(title=_(u"Author email address"), 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"""

View File

@ -0,0 +1,9 @@
<?xml version="1.0"?>
<componentregistry>
<utilities>
<utility
interface="plone.app.discussion.interfaces.ICommentingTool"
factory="plone.app.discussion.tool.CommentingTool"
/>
</utilities>
</componentregistry>

View File

@ -15,6 +15,7 @@ from Products.PloneTestCase.layer import PloneSite
from plone.app.discussion.conversation import Conversation from plone.app.discussion.conversation import Conversation
from plone.app.discussion.comment import Comment from plone.app.discussion.comment import Comment
from plone.app.discussion.interfaces import ICommentingTool, IConversation
class ConversationTest(TestCase): class ConversationTest(TestCase):
def afterSetUp(self): def afterSetUp(self):
@ -27,7 +28,7 @@ class ConversationTest(TestCase):
def test_add_comment(self): def test_add_comment(self):
# Create a conversation. In this case we doesn't assign it to an # Create a conversation. In this case we doesn't assign it to an
# object, as we just want to check the Conversation object API. # 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 # Add a comment. reply_to=0 means it's not a reply
comment = Comment(conversation=conversation, reply_to=0) comment = Comment(conversation=conversation, reply_to=0)
@ -43,7 +44,6 @@ class ConversationTest(TestCase):
self.assertEquals(conversation.total_comments, 1) self.assertEquals(conversation.total_comments, 1)
self.assert_(conversation.last_comment_date - datetime.now() < timedelta(seconds=1)) self.assert_(conversation.last_comment_date - datetime.now() < timedelta(seconds=1))
def test_suite(): def test_suite():
return unittest.TestSuite([ return unittest.TestSuite([
unittest.makeSuite(ConversationTest), unittest.makeSuite(ConversationTest),

View File

@ -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')

View File

@ -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)