- specify dependencies

- set profile version to 1 (profile version != package version)
- complete dict APIs and IReplies adapters (not yet fully tested)
- tidy up addComment() and __delitem__ w.r.t events
- sync interfaces with actual code
- move tests to use collective.testcaselayer

svn path=/plone.app.discussion/trunk/; revision=27010
This commit is contained in:
Martin Aspeli
2009-05-18 14:16:48 +00:00
parent b1c225176b
commit 22ae84735b
12 changed files with 317 additions and 147 deletions
+168 -44
View File
@@ -10,6 +10,8 @@ manipulating the comments directly in reply to a particular comment or at the
top level of the conversation.
"""
import time
from persistent import Persistent
from zope.interface import implements, implementer
@@ -17,18 +19,22 @@ 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 Acquisition import Explicit
from OFS.Traversable import Traversable
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 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
@@ -37,13 +43,13 @@ 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
from Acquisition import aq_base
ANNO_KEY = 'plone.app.discussion:conversation'
class Conversation(Persistent, Explicit):
class Conversation(Traversable, Persistent, Explicit):
"""A conversation is a container for all comments on a content object.
It manages internal data structures for comment threading and efficient
@@ -57,7 +63,6 @@ class Conversation(Persistent, Explicit):
# username -> count of comments; key is removed when count reaches 0
self._commentators = OIBTree()
self._last_comment_date = None
# id -> comment - find comment by id
self._comments = LOBTree()
@@ -66,55 +71,76 @@ class Conversation(Persistent, Explicit):
self._children = LOBTree()
def getId(self):
"""
"""Get the id of
"""
return self.id
@property
def enabled(self):
# TODO
# TODO - check __parent__'s settings + global settings
return True
@property
def total_comments(self):
# TODO
return len(self._comments)
@property
def last_comment_date(self):
return self._last_comment_date
try:
return self._comments[self._comments.maxKey()].creation_date
except (ValueError, KeyError, AttributeError,):
return None
@property
def commentators(self):
# TODO:
return set()
return self._commentators.keys()
def getComments(self, start=0, size=None):
"""Get unthreaded comments
"""
# TODO - batching
return self._comments.values()
def getThreads(self, start=0, size=None, root=None, depth=None):
# TODO:
"""Get threaded comments
"""
# TODO - build threads
return self._comments.values()
def addComment(self, comment):
id = comment.comment_id
if id in self._comments:
id = max(self._comments.keys()) + 1
"""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
returned.
"""
# Make sure we don't have a wrapped object
comment = aq_base(comment)
id = long(time.time() * 1e6)
while id in self._comments:
id += 1
comment.comment_id = id
notify(ObjectWillBeAddedEvent(comment, self, id))
self._comments[id] = comment
comment.comment_id = id
commentator = comment.creator
if not commentator in self._commentators:
self._commentators[commentator] = 0
self._commentators[commentator] += 1
self._last_comment_date = comment.creation_date
# for logged in users only
commentator = comment.author_username
if commentator:
if not commentator in self._commentators:
self._commentators[commentator] = 0
self._commentators[commentator] += 1
reply_to = comment.in_reply_to
if not reply_to:
# top level comments are in reply to the faux id 0
comment.in_reply_to = reply_to = 0
if not reply_to in self._children:
self._children[reply_to] = LLSet()
self._children[reply_to].insert(id)
# Notify that the object is added. The object must here be
# acquisition wrapped or the indexing will fail.
notify(ObjectAddedEvent(comment.__of__(self), self, id))
@@ -122,27 +148,63 @@ class Conversation(Persistent, Explicit):
# Dict API
def __len__(self):
return len(self._comments)
def __contains__(self, key):
return long(key) in self._comments
# TODO: Should __getitem__, get, __iter__, values(), items() and iter* return aq-wrapped comments?
def __getitem__(self, key):
return self._comments[key]
"""Get an item by its long key
"""
return self._comments[long(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)
"""Delete an item by its long key
"""
key = long(key)
comment = self[key]
commentator = comment.author_username
notify(ObjectWillBeRemovedEvent(comment, self, key))
self._comments.remove(key)
notify(ObjectRemovedEvent(comment, self, key))
if commentator and commentator in self._commentators:
if self._commentators[commentator] <= 1:
del self._commentators[commentator]
else:
self._commentators[commentator] -= 1
notify(ContainerModifiedEvent(self))
def __iter__(self):
return iter(self._comments)
def get(self, key, default=None):
return self._comments.get(long(key), default)
def keys(self):
return self._comments.keys()
def getPhysicalPath(self):
return self.aq_parent.getPhysicalPath() + (self.id,)
def items(self):
return self._comments.items()
# TODO: Update internal data structures when items added or removed
def values(self):
return self._comments.values()
def iterkeys(self):
return self._comments.iterkeys()
def itervalues(self):
return self._comments.itervalues()
def iteritems(self):
return self._comments.iteritems()
@implementer(IConversation)
@adapter(IAnnotatable)
@@ -155,7 +217,6 @@ def conversationAdapterFactory(content):
conversation._parent_uid = content.UID()
annotions[ANNO_KEY] = conversation
conversation = annotions[ANNO_KEY]
# Probably this needs an acquisition wrapper
return conversation
class ConversationReplies(object):
@@ -165,14 +226,70 @@ class ConversationReplies(object):
"""
implements(IReplies)
adapts(Conversation)
adapts(Conversation) # relies on implementation details
def __init__(self, context):
self.conversation = context
self.root = 0
self.children = self.conversation._children.get(0, LLSet())
# TODO: dict interface - generalise to work with any starting point, so
# that the subclassing below works
def addComment(self, comment):
comment.in_reply_to = None
return self.conversation.addComment(comment)
# Dict API
def __len__(self):
return len(self.children)
def __contains__(self, key):
return long(key) in self.children
# TODO: Should __getitem__, get, __iter__, values(), items() and iter* return aq-wrapped comments?
def __getitem__(self, key):
"""Get an item by its long key
"""
key = long(key)
if key not in self.children:
raise KeyError(key)
return self.conversation[key]
def __delitem__(self, key):
"""Delete an item by its long key
"""
key = long(key)
if key not in self.children:
raise KeyError(key)
del self.conversation[key]
def __iter__(self):
return iter(self.children)
def get(self, key, default=None):
key = long(key)
if key not in self.children:
return default
return self.conversation.get(key)
def keys(self):
return self.children
def items(self):
return [(k, self.conversation[k]) for k in self.children]
def values(self):
return [self.conversation[k] for k in self.children]
def iterkeys(self):
return iter(self.children)
def itervalues(self):
for key in self.children:
yield self.conversation[key]
def iteritems(self):
for key in self.children:
yield (key, self.conversation[key],)
class CommentReplies(ConversationReplies):
"""An IReplies adapter for comments.
@@ -184,5 +301,12 @@ class CommentReplies(ConversationReplies):
adapts(IComment)
def __init__(self, context):
self.conversation = context.__parent__
self.root = context.comment_id
self.comment = context
self.comment_id = context.comment_id
self.children = self.conversation._children.get(0, LLSet())
def addComment(self, comment):
comment.in_reply_to = self.comment_id
return self.conversation.addComment(comment)
# Dict API is inherited