X-Git-Url: http://git.veekun.com/zzz-floof.git/blobdiff_plain/fca6f64bc81a02c61ba45a13de25f3da2e7630b3..db4c5f0b56d22815ecc31754271797b2401c9458:/floof/model/comments.py?ds=sidebyside diff --git a/floof/model/comments.py b/floof/model/comments.py index 4504b6f..ef3a948 100644 --- a/floof/model/comments.py +++ b/floof/model/comments.py @@ -1,15 +1,120 @@ 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)) + # 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') + + 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