Example #1
0
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),)