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:
Timo Stollenwerk 2009-05-23 14:18:35 +00:00
parent 2ff696a252
commit 8282307e0a
2 changed files with 99 additions and 76 deletions

View File

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

View File

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