More thorough support for comment threading.
[zzz-floof.git] / floof / model / comments.py
1 import datetime
2
3 from elixir import *
4 from sqlalchemy import func
5
6 ### Utility function(s)
7
8 def indent_comments(comments):
9 """Given a list of comment objects, returns the same comments (and changes
10 them in-place) with an `indent` property set on each comment. This
11 indicates how deeply nested each comment is, relative to the first comment.
12 The first comment's indent level is 0.
13
14 The comments must be a complete subtree, ordered by their `left` property.
15 """
16
17 last_comment = None
18 indent = 0
19 right_ancestry = []
20 for comment in comments:
21 # If this comment is a child of the last, bump the nesting level
22 if last_comment and comment.left < last_comment.right:
23 indent = indent + 1
24 # Remember current ancestory relevant to the root
25 right_ancestry.append(last_comment)
26
27 # On the other hand, for every nesting level this comment may have just
28 # broken out of, back out a level
29 for i in xrange(len(right_ancestry) - 1, -1, -1):
30 if comment.left > right_ancestry[i].right:
31 indent = indent - 1
32 right_ancestry.pop(i)
33
34 # Cache parent comment
35 if len(right_ancestry):
36 comment._parent = right_ancestry[-1]
37
38 comment.indent = indent
39
40 last_comment = comment
41
42 return comments
43
44
45 ### Usual database classes
46
47 class Discussion(Entity):
48 """Represents a collection of comments attached to some other object."""
49 count = Field(Integer)
50 comments = OneToMany('Comment', order_by='left')
51
52 class Comment(Entity):
53 time = Field(DateTime, default=datetime.datetime.now)
54 text = Field(Unicode(65536))
55
56 # Comments are a tree, and are stored as a nested set, because:
57 # - It's easy to get a subtree with a single query.
58 # - It's easy to get a comment's depth.
59 # - It's easy to sort comments without recursion.
60 # The only real disadvantage is that adding a comment requires a quick
61 # update of all the following comments (in post-order), but that's rare
62 # enough that it shouldn't be a problem.
63 left = Field(Integer, index=True)
64 right = Field(Integer)
65
66 discussion = ManyToOne('Discussion')
67 user = ManyToOne('User')
68
69 def __init__(self, parent=None, **kwargs):
70 """Constructor override to set left/right correctly on a new comment.
71 """
72 super(Comment, self).__init__(**kwargs)
73
74 # Keep the comment count updated
75 self.discussion.count += 1
76
77 if parent:
78 # Parent comment given.
79 parent_right = parent.right
80 # Shift every left/right ahead by two to make room for the new
81 # comment's left/right
82 Comment.query.filter(Comment.discussion == self.discussion) \
83 .filter(Comment.left >= parent_right) \
84 .update({ Comment.left: Comment.left + 2 })
85 Comment.query.filter(Comment.discussion == self.discussion) \
86 .filter(Comment.right >= parent_right) \
87 .update({ Comment.right: Comment.right + 2 })
88
89 # Then stick the new comment in the right place
90 self.left = parent_right
91 self.right = parent_right + 1
92
93 else:
94 query = session.query(func.max(Comment.right)) \
95 .filter(Comment.discussion == self.discussion)
96 (max_right,) = query.one()
97
98 if not max_right:
99 # No comments yet. Use 1 and 2
100 self.left = 1
101 self.right = 2
102 else:
103 self.left = max_right + 1
104 self.right = max_right + 2
105