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 comment lookup. """ implements(IConversation, IHideFromBreadcrumbs) __allow_access_to_unprotected_subobjects__ = True def __init__(self, id="++conversation++default"): self.id = id # username -> count of comments; key is removed when count reaches 0 self._commentators = OIBTree() # id -> comment - find comment by id self._comments = LOBTree() # id -> LLSet (children) - find all children for a given comment. # 0 signifies root. self._children = LOBTree() def getId(self): """Get the id of the conversation. This is used to construct a URL. """ return self.id def enabled(self): parent = aq_inner(self.__parent__) return parent.restrictedTraverse('@@conversation_view').enabled() @property def total_comments(self): public_comments = [ x for x in self._comments.values() if user_nobody.has_permission('View', x) ] return len(public_comments) @property def last_comment_date(self): # self._comments is an Instance of a btree. The keys # are always ordered comment_keys = self._comments.keys() for comment_key in reversed(comment_keys): comment = self._comments[comment_key] if user_nobody.has_permission('View', comment): return comment.creation_date return None @property def commentators(self): return self._commentators @property def public_commentators(self): retval = set() for comment in self._comments.values(): if not user_nobody.has_permission('View', comment): continue retval.add(comment.author_username) return tuple(retval) def objectIds(self): return self._comments.keys() def getComments(self, start=0, size=None): """Get unthreaded comments """ count = 0l for comment in self._comments.values(min=start): # Yield the acquisition wrapped comment yield self[comment.id] count += 1 if size and count > size: return def getThreads(self, start=0, size=None, root=0, depth=None): """Get threaded comments """ def recurse(comment_id, d=0): # Yield the current comment before we look for its children yield {'id': comment_id, 'comment': self[comment_id], 'depth': d} # Recurse if there are children and we are not out of our depth if depth is None or d + 1 < depth: children = self._children.get(comment_id, None) if children is not None: for child_id in children: for value in recurse(child_id, d + 1): yield value # Find top level threads comments = self._children.get(root, None) if comments is not None: count = 0l for comment_id in comments.keys(min=start): # Abort if we have found all the threads we want count += 1 if size and count > size: return # Let the closure recurse for value in recurse(comment_id): yield value def addComment(self, comment): """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.__parent__ = aq_base(self) # Record unique users who've commented (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) # Add the annotation if not already done annotions = IAnnotations(self.__parent__) if not ANNOTATION_KEY in annotions: annotions[ANNOTATION_KEY] = aq_base(self) # Notify that the object is added. The object must here be # acquisition wrapped or the indexing will fail. notify(ObjectCreatedEvent(comment)) notify(ObjectAddedEvent(comment.__of__(self), self, id)) notify(ContainerModifiedEvent(self)) return id # Dict API def __len__(self): return len(self._comments) def __contains__(self, key): return long(key) in self._comments def __getitem__(self, key): """Get an item by its long key """ try: comment_id = long(key) except ValueError: return return self._comments[comment_id].__of__(self) def __delitem__(self, key, suppress_container_modified=False): """Delete an item by its long key """ key = long(key) comment = self[key].__of__(self) commentator = comment.author_username notify(ObjectWillBeRemovedEvent(comment, self, key)) # Remove all children for child_id in self._children.get(key, []): # avoid sending ContainerModifiedEvent multiple times self.__delitem__(child_id, suppress_container_modified=True) # Remove the comment from _comments self._comments.pop(key) # Remove this comment as a child of its parent if not suppress_container_modified: parent = comment.in_reply_to if parent is not None: parent_children = self._children.get(parent, None) if parent_children is not None and key in parent_children: parent_children.remove(key) # Remove commentators if commentator and commentator in self._commentators: if self._commentators[commentator] <= 1: del self._commentators[commentator] else: self._commentators[commentator] -= 1 notify(ObjectRemovedEvent(comment, self, key)) if not suppress_container_modified: notify(ContainerModifiedEvent(self)) def __iter__(self): return iter(self._comments) def get(self, key, default=None): comment = self._comments.get(long(key), default) if comment is default: return default return comment.__of__(self) def keys(self): return self._comments.keys() def items(self): return [( i[0], i[1].__of__(self), ) for i in self._comments.items()] def values(self): return [v.__of__(self) for v in self._comments.values()] def iterkeys(self): return self._comments.iterkeys() def itervalues(self): for v in self._comments.itervalues(): yield v.__of__(self) def iteritems(self): for k, v in self._comments.iteritems(): yield ( k, v.__of__(self), )
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 comment lookup. """ implements(IConversation) __allow_access_to_unprotected_subobjects__ = True def __init__(self, id="++conversation++default"): self.id = id # username -> count of comments; key is removed when count reaches 0 self._commentators = OIBTree() # id -> comment - find comment by id self._comments = LOBTree() # id -> LLSet (children) - find all children for a given comment. # 0 signifies root. self._children = LOBTree() def getId(self): """Get the id of the conversation. This is used to construct a URL. """ return self.id def enabled(self): parent = aq_inner(self.__parent__) return parent.restrictedTraverse('@@conversation_view').enabled() @property def total_comments(self): return len(self._comments) @property def last_comment_date(self): try: return self._comments[self._comments.maxKey()].creation_date except (ValueError, KeyError, AttributeError,): return None @property def commentators(self): return self._commentators def objectIds(self): return self._comments.keys() def getComments(self, start=0, size=None): """Get unthreaded comments """ count = 0l for comment in self._comments.values(min=start): # Yield the acquisition wrapped comment yield self[comment.id] count += 1 if size and count > size: return def getThreads(self, start=0, size=None, root=0, depth=None): """Get threaded comments """ def recurse(comment_id, d=0): # Yield the current comment before we look for its children yield {'id': comment_id, 'comment': self[comment_id], 'depth': d} # Recurse if there are children and we are not out of our depth if depth is None or d + 1 < depth: children = self._children.get(comment_id, None) if children is not None: for child_id in children: for value in recurse(child_id, d+1): yield value # Find top level threads comments = self._children.get(root, None) if comments is not None: count = 0l for comment_id in comments.keys(min=start): # Abort if we have found all the threads we want count += 1 if size and count > size: return # Let the closure recurse for value in recurse(comment_id): yield value def addComment(self, comment): """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.__parent__ = aq_base(self) # Record unique users who've commented (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) # Add the annotation if not already done annotions = IAnnotations(self.__parent__) if not ANNOTATION_KEY in annotions: annotions[ANNOTATION_KEY] = aq_base(self) # Notify that the object is added. The object must here be # acquisition wrapped or the indexing will fail. notify(ObjectCreatedEvent(comment)) notify(ObjectAddedEvent(comment.__of__(self), self, id)) notify(ContainerModifiedEvent(self)) return id # Dict API def __len__(self): return len(self._comments) def __contains__(self, key): return long(key) in self._comments def __getitem__(self, key): """Get an item by its long key """ return self._comments[long(key)].__of__(self) def __delitem__(self, key, suppress_container_modified=False): """Delete an item by its long key """ key = long(key) comment = self[key].__of__(self) commentator = comment.author_username notify(ObjectWillBeRemovedEvent(comment, self, key)) # Remove all children for child_id in self._children.get(key, []): # avoid sending ContainerModifiedEvent multiple times self.__delitem__(child_id, suppress_container_modified=True) # Remove the comment from _comments self._comments.pop(key) # Remove this comment as a child of its parent if not suppress_container_modified: parent = comment.in_reply_to if parent is not None: parent_children = self._children.get(parent, None) if parent_children is not None and key in parent_children: parent_children.remove(key) # Remove commentators if commentator and commentator in self._commentators: if self._commentators[commentator] <= 1: del self._commentators[commentator] else: self._commentators[commentator] -= 1 notify(ObjectRemovedEvent(comment, self, key)) if not suppress_container_modified: notify(ContainerModifiedEvent(self)) def __iter__(self): return iter(self._comments) def get(self, key, default=None): comment = self._comments.get(long(key), default) if comment is default: return default return comment.__of__(self) def keys(self): return self._comments.keys() def items(self): return [(i[0], i[1].__of__(self),) for i in self._comments.items()] def values(self): return [v.__of__(self) for v in self._comments.values()] def iterkeys(self): return self._comments.iterkeys() def itervalues(self): for v in self._comments.itervalues(): yield v.__of__(self) def iteritems(self): for k, v in self._comments.iteritems(): yield (k, v.__of__(self),)