fix conversation.__delitem__ function and add test for deleting comments from a conversation.
svn path=/plone.app.discussion/trunk/; revision=27062
This commit is contained in:
parent
2ff696a252
commit
8282307e0a
@ -51,167 +51,168 @@ ANNOTATION_KEY = 'plone.app.discussion:conversation'
|
|||||||
|
|
||||||
class Conversation(Traversable, Persistent, Explicit):
|
class Conversation(Traversable, 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.
|
||||||
|
|
||||||
It manages internal data structures for comment threading and efficient
|
It manages internal data structures for comment threading and efficient
|
||||||
comment lookup.
|
comment lookup.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
implements(IConversation)
|
implements(IConversation)
|
||||||
|
|
||||||
__allow_access_to_unprotected_subobjects__ = True
|
__allow_access_to_unprotected_subobjects__ = True
|
||||||
|
|
||||||
def __init__(self, id="++conversation++default"):
|
def __init__(self, id="++conversation++default"):
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
# username -> count of comments; key is removed when count reaches 0
|
# username -> count of comments; key is removed when count reaches 0
|
||||||
self._commentators = OIBTree()
|
self._commentators = OIBTree()
|
||||||
|
|
||||||
# id -> comment - find comment by id
|
# id -> comment - find comment by id
|
||||||
self._comments = LOBTree()
|
self._comments = LOBTree()
|
||||||
|
|
||||||
# id -> IISet (children) - find all children for a given comment. 0 signifies root.
|
# id -> IISet (children) - find all children for a given comment. 0 signifies root.
|
||||||
self._children = LOBTree()
|
self._children = LOBTree()
|
||||||
|
|
||||||
def getId(self):
|
def getId(self):
|
||||||
"""Get the id of the conversation. This is used to construct a
|
"""Get the id of the conversation. This is used to construct a
|
||||||
URL.
|
URL.
|
||||||
"""
|
"""
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self):
|
def enabled(self):
|
||||||
# TODO - check __parent__'s settings + global settings
|
# TODO - check __parent__'s settings + global settings
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_comments(self):
|
def total_comments(self):
|
||||||
return len(self._comments)
|
return len(self._comments)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_comment_date(self):
|
def last_comment_date(self):
|
||||||
try:
|
try:
|
||||||
return self._comments[self._comments.maxKey()].creation_date
|
return self._comments[self._comments.maxKey()].creation_date
|
||||||
except (ValueError, KeyError, AttributeError,):
|
except (ValueError, KeyError, AttributeError,):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def commentators(self):
|
def commentators(self):
|
||||||
return self._commentators.keys()
|
return self._commentators.keys()
|
||||||
|
|
||||||
def getComments(self, start=0, size=None):
|
def getComments(self, start=0, size=None):
|
||||||
"""Get unthreaded comments
|
"""Get unthreaded comments
|
||||||
"""
|
"""
|
||||||
# TODO - batching
|
# TODO - batching
|
||||||
return self._comments.values()
|
return self._comments.values()
|
||||||
|
|
||||||
def getThreads(self, start=0, size=None, root=None, depth=None):
|
def getThreads(self, start=0, size=None, root=None, depth=None):
|
||||||
"""Get threaded comments
|
"""Get threaded comments
|
||||||
"""
|
"""
|
||||||
# TODO - build threads
|
# TODO - build threads
|
||||||
return self._comments.values()
|
return self._comments.values()
|
||||||
|
|
||||||
def addComment(self, comment):
|
def addComment(self, comment):
|
||||||
"""Add a new comment. The parent id should have been set already. The
|
"""Add a new comment. The parent id should have been set already. The
|
||||||
comment id may be modified to find a free key. The id used will be
|
comment id may be modified to find a free key. The id used will be
|
||||||
returned.
|
returned.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Make sure we don't have a wrapped object
|
# Make sure we don't have a wrapped object
|
||||||
|
|
||||||
comment = aq_base(comment)
|
comment = aq_base(comment)
|
||||||
|
|
||||||
id = long(time.time() * 1e6)
|
id = long(time.time() * 1e6)
|
||||||
while id in self._comments:
|
while id in self._comments:
|
||||||
id += 1
|
id += 1
|
||||||
|
|
||||||
comment.comment_id = id
|
comment.comment_id = id
|
||||||
notify(ObjectWillBeAddedEvent(comment, self, id))
|
notify(ObjectWillBeAddedEvent(comment, self, id))
|
||||||
self._comments[id] = comment
|
self._comments[id] = comment
|
||||||
|
|
||||||
comment.__parent__ = self
|
comment.__parent__ = self
|
||||||
|
|
||||||
# Record unique users who've commented (for logged in users only)
|
# Record unique users who've commented (for logged in users only)
|
||||||
commentator = comment.author_username
|
commentator = comment.author_username
|
||||||
if commentator:
|
if commentator:
|
||||||
if not commentator in self._commentators:
|
if not commentator in self._commentators:
|
||||||
self._commentators[commentator] = 0
|
self._commentators[commentator] = 0
|
||||||
self._commentators[commentator] += 1
|
self._commentators[commentator] += 1
|
||||||
|
|
||||||
reply_to = comment.in_reply_to
|
reply_to = comment.in_reply_to
|
||||||
if not reply_to:
|
if not reply_to:
|
||||||
# top level comments are in reply to the faux id 0
|
# top level comments are in reply to the faux id 0
|
||||||
comment.in_reply_to = reply_to = 0
|
comment.in_reply_to = reply_to = 0
|
||||||
|
|
||||||
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 that the object is added. The object must here be
|
# Notify that the object is added. The object must here be
|
||||||
# acquisition wrapped or the indexing will fail.
|
# acquisition wrapped or the indexing will fail.
|
||||||
notify(ObjectAddedEvent(comment.__of__(self), self, id))
|
notify(ObjectAddedEvent(comment.__of__(self), self, id))
|
||||||
notify(ContainerModifiedEvent(self))
|
notify(ContainerModifiedEvent(self))
|
||||||
|
|
||||||
return id
|
return id
|
||||||
|
|
||||||
# Dict API
|
# Dict API
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self._comments)
|
return len(self._comments)
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
return long(key) in self._comments
|
return long(key) in self._comments
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
"""Get an item by its long key
|
"""Get an item by its long key
|
||||||
"""
|
"""
|
||||||
return self._comments[long(key)].__of__(self)
|
return self._comments[long(key)].__of__(self)
|
||||||
|
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
"""Delete an item by its long key
|
"""Delete an item by its long key
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = long(key)
|
key = long(key)
|
||||||
|
|
||||||
comment = self[key].__of__(self)
|
comment = self[key].__of__(self)
|
||||||
commentator = comment.author_username
|
commentator = comment.author_username
|
||||||
|
|
||||||
notify(ObjectWillBeRemovedEvent(comment, self, key))
|
notify(ObjectWillBeRemovedEvent(comment, self, key))
|
||||||
self._comments.remove(key)
|
|
||||||
|
self._comments.pop(key)
|
||||||
notify(ObjectRemovedEvent(comment, self, key))
|
notify(ObjectRemovedEvent(comment, self, key))
|
||||||
|
|
||||||
if commentator and commentator in self._commentators:
|
if commentator and commentator in self._commentators:
|
||||||
if self._commentators[commentator] <= 1:
|
if self._commentators[commentator] <= 1:
|
||||||
del self._commentators[commentator]
|
del self._commentators[commentator]
|
||||||
else:
|
else:
|
||||||
self._commentators[commentator] -= 1
|
self._commentators[commentator] -= 1
|
||||||
|
|
||||||
notify(ContainerModifiedEvent(self))
|
notify(ContainerModifiedEvent(self))
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self._comments)
|
return iter(self._comments)
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
comment = self._comments.get(long(key), default)
|
comment = self._comments.get(long(key), default)
|
||||||
if comment is default:
|
if comment is default:
|
||||||
return default
|
return default
|
||||||
return comment.__of__(self)
|
return comment.__of__(self)
|
||||||
|
|
||||||
def keys(self):
|
def keys(self):
|
||||||
return self._comments.keys()
|
return self._comments.keys()
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
return [(i[0], i[1].__of__(self),) for i in self._comments.items()]
|
return [(i[0], i[1].__of__(self),) for i in self._comments.items()]
|
||||||
|
|
||||||
def values(self):
|
def values(self):
|
||||||
return [v.__of__(self) for v in self._comments.values()]
|
return [v.__of__(self) for v in self._comments.values()]
|
||||||
|
|
||||||
def iterkeys(self):
|
def iterkeys(self):
|
||||||
return self._comments.iterkeys()
|
return self._comments.iterkeys()
|
||||||
|
|
||||||
def itervalues(self):
|
def itervalues(self):
|
||||||
for v in self._comments.itervalues():
|
for v in self._comments.itervalues():
|
||||||
yield v.__of__(self)
|
yield v.__of__(self)
|
||||||
|
|
||||||
def iteritems(self):
|
def iteritems(self):
|
||||||
for k, v in self._comments.iteritems():
|
for k, v in self._comments.iteritems():
|
||||||
yield (k, v.__of__(self),)
|
yield (k, v.__of__(self),)
|
||||||
@ -231,31 +232,31 @@ def conversationAdapterFactory(content):
|
|||||||
|
|
||||||
class ConversationReplies(object):
|
class ConversationReplies(object):
|
||||||
"""An IReplies adapter for conversations.
|
"""An IReplies adapter for conversations.
|
||||||
|
|
||||||
This makes it easy to work with top-level comments.
|
This makes it easy to work with top-level comments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
implements(IReplies)
|
implements(IReplies)
|
||||||
adapts(Conversation) # relies on implementation details
|
adapts(Conversation) # relies on implementation details
|
||||||
|
|
||||||
def __init__(self, context):
|
def __init__(self, context):
|
||||||
self.conversation = context
|
self.conversation = context
|
||||||
self.children = self.conversation._children.get(0, LLSet())
|
self.children = self.conversation._children.get(0, LLSet())
|
||||||
|
|
||||||
def addComment(self, comment):
|
def addComment(self, comment):
|
||||||
comment.in_reply_to = None
|
comment.in_reply_to = None
|
||||||
return self.conversation.addComment(comment)
|
return self.conversation.addComment(comment)
|
||||||
|
|
||||||
# Dict API
|
# Dict API
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self.children)
|
return len(self.children)
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
return long(key) in self.children
|
return long(key) in self.children
|
||||||
|
|
||||||
# TODO: Should __getitem__, get, __iter__, values(), items() and iter* return aq-wrapped comments?
|
# TODO: Should __getitem__, get, __iter__, values(), items() and iter* return aq-wrapped comments?
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
"""Get an item by its long key
|
"""Get an item by its long key
|
||||||
"""
|
"""
|
||||||
@ -263,7 +264,7 @@ class ConversationReplies(object):
|
|||||||
if key not in self.children:
|
if key not in self.children:
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
return self.conversation[key]
|
return self.conversation[key]
|
||||||
|
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
"""Delete an item by its long key
|
"""Delete an item by its long key
|
||||||
"""
|
"""
|
||||||
@ -271,42 +272,42 @@ class ConversationReplies(object):
|
|||||||
if key not in self.children:
|
if key not in self.children:
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
del self.conversation[key]
|
del self.conversation[key]
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.children)
|
return iter(self.children)
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
key = long(key)
|
key = long(key)
|
||||||
if key not in self.children:
|
if key not in self.children:
|
||||||
return default
|
return default
|
||||||
return self.conversation.get(key)
|
return self.conversation.get(key)
|
||||||
|
|
||||||
def keys(self):
|
def keys(self):
|
||||||
return self.children
|
return self.children
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
return [(k, self.conversation[k]) for k in self.children]
|
return [(k, self.conversation[k]) for k in self.children]
|
||||||
|
|
||||||
def values(self):
|
def values(self):
|
||||||
return [self.conversation[k] for k in self.children]
|
return [self.conversation[k] for k in self.children]
|
||||||
|
|
||||||
def iterkeys(self):
|
def iterkeys(self):
|
||||||
return iter(self.children)
|
return iter(self.children)
|
||||||
|
|
||||||
def itervalues(self):
|
def itervalues(self):
|
||||||
for key in self.children:
|
for key in self.children:
|
||||||
yield self.conversation[key]
|
yield self.conversation[key]
|
||||||
|
|
||||||
def iteritems(self):
|
def iteritems(self):
|
||||||
for key in self.children:
|
for key in self.children:
|
||||||
yield (key, self.conversation[key],)
|
yield (key, self.conversation[key],)
|
||||||
|
|
||||||
class CommentReplies(ConversationReplies):
|
class CommentReplies(ConversationReplies):
|
||||||
"""An IReplies adapter for comments.
|
"""An IReplies adapter for comments.
|
||||||
|
|
||||||
This makes it easy to work with replies to specific comments.
|
This makes it easy to work with replies to specific comments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
implements(IReplies)
|
implements(IReplies)
|
||||||
|
|
||||||
# depends on implementation details of conversation
|
# depends on implementation details of conversation
|
||||||
@ -314,20 +315,20 @@ class CommentReplies(ConversationReplies):
|
|||||||
# have a different type of Comment
|
# have a different type of Comment
|
||||||
|
|
||||||
adapts(Comment)
|
adapts(Comment)
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, context):
|
def __init__(self, context):
|
||||||
self.comment = context
|
self.comment = context
|
||||||
self.conversation = self.comment.__parent__
|
self.conversation = self.comment.__parent__
|
||||||
|
|
||||||
if self.conversation is None or not hasattr(self.conversation, '_children'):
|
if self.conversation is None or not hasattr(self.conversation, '_children'):
|
||||||
raise TypeError("This adapter doesn't know what to do with the parent conversation")
|
raise TypeError("This adapter doesn't know what to do with the parent conversation")
|
||||||
|
|
||||||
self.comment_id = self.comment.comment_id
|
self.comment_id = self.comment.comment_id
|
||||||
self.children = self.conversation._children.get(self.comment_id, LLSet())
|
self.children = self.conversation._children.get(self.comment_id, LLSet())
|
||||||
|
|
||||||
def addComment(self, comment):
|
def addComment(self, comment):
|
||||||
comment.in_reply_to = self.comment_id
|
comment.in_reply_to = self.comment_id
|
||||||
return self.conversation.addComment(comment)
|
return self.conversation.addComment(comment)
|
||||||
|
|
||||||
# Dict API is inherited, written in terms of self.conversation and self.children
|
# Dict API is inherited, written in terms of self.conversation and self.children
|
||||||
|
@ -48,7 +48,29 @@ class ConversationTest(PloneTestCase):
|
|||||||
self.assert_(conversation.last_comment_date - datetime.now() < timedelta(seconds=1))
|
self.assert_(conversation.last_comment_date - datetime.now() < timedelta(seconds=1))
|
||||||
|
|
||||||
def test_delete_comment(self):
|
def test_delete_comment(self):
|
||||||
pass
|
# 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)
|
||||||
|
|
||||||
|
# Pretend that we have traversed to the comment by aq wrapping it.
|
||||||
|
conversation = conversation.__of__(self.portal.doc1)
|
||||||
|
|
||||||
|
# Add a comment. Note: in real life, we always create comments via the factory
|
||||||
|
# to allow different factories to be swapped in
|
||||||
|
|
||||||
|
comment = createObject('plone.Comment')
|
||||||
|
comment.title = 'Comment 1'
|
||||||
|
comment.text = 'Comment text'
|
||||||
|
|
||||||
|
new_id = conversation.addComment(comment)
|
||||||
|
|
||||||
|
# delete the comment we just created
|
||||||
|
conversation.__delitem__(new_id)
|
||||||
|
|
||||||
|
# make sure there is no comment left in the conversation
|
||||||
|
self.assertEquals(len(conversation.getComments()), 0)
|
||||||
|
self.assertEquals(len(conversation.getThreads()), 0)
|
||||||
|
self.assertEquals(conversation.total_comments, 0)
|
||||||
|
|
||||||
def test_dict_operations(self):
|
def test_dict_operations(self):
|
||||||
# test dict operations and acquisition wrapping
|
# test dict operations and acquisition wrapping
|
||||||
@ -103,13 +125,13 @@ class ConversationTest(PloneTestCase):
|
|||||||
|
|
||||||
def test_get_threads_batched(self):
|
def test_get_threads_batched(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_traversal(self):
|
def test_traversal(self):
|
||||||
# make sure we can traverse to conversations and get a URL and path
|
# make sure we can traverse to conversations and get a URL and path
|
||||||
|
|
||||||
conversation = self.portal.doc1.restrictedTraverse('++conversation++default')
|
conversation = self.portal.doc1.restrictedTraverse('++conversation++default')
|
||||||
self.assert_(IConversation.providedBy(conversation))
|
self.assert_(IConversation.providedBy(conversation))
|
||||||
|
|
||||||
self.assertEquals(('', 'plone', 'doc1', '++conversation++default'), conversation.getPhysicalPath())
|
self.assertEquals(('', 'plone', 'doc1', '++conversation++default'), conversation.getPhysicalPath())
|
||||||
self.assertEquals('plone/doc1/%2B%2Bconversation%2B%2Bdefault', conversation.absolute_url())
|
self.assertEquals('plone/doc1/%2B%2Bconversation%2B%2Bdefault', conversation.absolute_url())
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user