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)
- comments = OneToMany('Comment')
+ comments = OneToMany('Comment', order_by='left')
class Comment(Entity):
- time = Field(DateTime, default=datetime.datetime.now)
- text = Field(Unicode(65536))
+ time = Field(DateTime, default=datetime.datetime.now, required=True)
+ text = Field(Unicode(65536), required=True)
+
+ # 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, required=True)
+ right = Field(Integer, required=True)
+
+ discussion = ManyToOne('Discussion', required=True)
+ user = ManyToOne('User', required=True)
+
+ 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
- discussion = ManyToOne('Discussion')
- user = ManyToOne('User')
+ @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