Merged add_tags and lib.search into lib.tags.
[zzz-floof.git] / floof / model / comments.py
index 4504b6f..ef3a948 100644 (file)
 import datetime
 
 from elixir import *
 import datetime
 
 from elixir import *
+from sqlalchemy import func
+
+### Utility function(s)
+
+def indent_comments(comments):
+    """Given a list of comment objects, returns the same comments (and changes
+    them in-place) with an `indent` property set on each comment.  This
+    indicates how deeply nested each comment is, relative to the first comment.
+    The first comment's indent level is 0.
+
+    The comments must be a complete subtree, ordered by their `left` property.
+
+    This function will also cache the `parent` property for each comment.
+    """
+
+    last_comment = None
+    indent = 0 
+    right_ancestry = []
+    for comment in comments:
+        # If this comment is a child of the last, bump the nesting level
+        if last_comment and comment.left < last_comment.right:
+            indent = indent + 1 
+            # Remember current ancestory relevant to the root
+            right_ancestry.append(last_comment)
+
+        # On the other hand, for every nesting level this comment may have just
+        # broken out of, back out a level
+        for i in xrange(len(right_ancestry) - 1, -1, -1):
+            if comment.left > right_ancestry[i].right:
+                indent = indent - 1 
+                right_ancestry.pop(i)
+
+        # Cache parent comment
+        if len(right_ancestry):
+            comment._parent = right_ancestry[-1]
+
+        comment.indent = indent
+
+        last_comment = comment
+
+    return comments
+
+
+### Usual database classes
 
 class Discussion(Entity):
     """Represents a collection of comments attached to some other object."""
     count = Field(Integer)
 
 class Discussion(Entity):
     """Represents a collection of comments attached to some other object."""
     count = Field(Integer)
-    comments = OneToMany('Comment')
+    comments = OneToMany('Comment', order_by='left')
 
 class Comment(Entity):
     time = Field(DateTime, default=datetime.datetime.now)
     text = Field(Unicode(65536))
 
 
 class Comment(Entity):
     time = Field(DateTime, default=datetime.datetime.now)
     text = Field(Unicode(65536))
 
+    # Comments are a tree, and are stored as a nested set, because:
+    # - It's easy to get a subtree with a single query.
+    # - It's easy to get a comment's depth.
+    # - It's easy to sort comments without recursion.
+    # The only real disadvantage is that adding a comment requires a quick
+    # update of all the following comments (in post-order), but that's rare
+    # enough that it shouldn't be a problem.
+    left = Field(Integer, index=True)
+    right = Field(Integer)
+
     discussion = ManyToOne('Discussion')
     user = ManyToOne('User')
     discussion = ManyToOne('Discussion')
     user = ManyToOne('User')
+
+    def __init__(self, parent=None, **kwargs):
+        """Constructor override to set left/right correctly on a new comment.
+        """
+        super(Comment, self).__init__(**kwargs)
+
+        # Keep the comment count updated
+        self.discussion.count += 1
+
+        if parent:
+            # Parent comment given.
+            parent_right = parent.right
+            # Shift every left/right ahead by two to make room for the new
+            # comment's left/right
+            Comment.query.filter(Comment.discussion == self.discussion) \
+                         .filter(Comment.left >= parent_right) \
+                         .update({ 'left': Comment.left + 2 })
+            Comment.query.filter(Comment.discussion == self.discussion) \
+                         .filter(Comment.right >= parent_right) \
+                         .update({ 'right': Comment.right + 2 })
+
+            # Then stick the new comment in the right place
+            self.left = parent_right
+            self.right = parent_right + 1
+
+        else:
+            query = session.query(func.max(Comment.right)) \
+                           .filter(Comment.discussion == self.discussion)
+            (max_right,) = query.one()
+
+            if not max_right:
+                # No comments yet.  Use 1 and 2
+                self.left = 1
+                self.right = 2
+            else:
+                self.left = max_right + 1
+                self.right = max_right + 2
+
+    @property
+    def parent(self):
+        """Returns this comment's parent.  This is cached, hence its being a
+        property and not a method.
+        """
+        if not hasattr(self, '_parent'):
+            self._parent = Comment.query \
+                .filter(Comment.discussion_id == self.discussion_id) \
+                .filter(Comment.left < self.left) \
+                .filter(Comment.right > self.right) \
+                .order_by(Comment.left.desc()) \
+                .first()
+        return self._parent