- 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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user