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:
parent
a2542ea5a8
commit
e35f761939
@ -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>
|
||||||
|
@ -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.
|
||||||
|
@ -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"""
|
||||||
|
|
@ -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>
|
@ -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),
|
||||||
|
52
plone/app/discussion/tests/test_tool.py
Normal file
52
plone/app/discussion/tests/test_tool.py
Normal 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')
|
59
plone/app/discussion/tool.py
Normal file
59
plone/app/discussion/tool.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user