Merge branch 'comments'
[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 This function will also cache the `parent` property for each comment.
17 """
18
19 last_comment = None
20 indent = 0
21 right_ancestry = []
22 for comment in comments:
23 # If this comment is a child of the last, bump the nesting level
24 if last_comment and comment.left < last_comment.right:
25 indent = indent + 1
26 # Remember current ancestory relevant to the root
27 right_ancestry.append(last_comment)
28
29 # On the other hand, for every nesting level this comment may have just
30 # broken out of, back out a level
31 for i in xrange(len(right_ancestry) - 1, -1, -1):
32 if comment.left > right_ancestry[i].right:
33 indent = indent - 1
34 right_ancestry.pop(i)
35
36 # Cache parent comment
37 if len(right_ancestry):
38 comment._parent = right_ancestry[-1]
39
40 comment.indent = indent
41
42 last_comment = comment
43
44 return comments
45
46
47 ### Usual database classes
48
49 class Discussion(Entity):
50 """Represents a collection of comments attached to some other object."""
51 count = Field(Integer)
52 comments = OneToMany('Comment', order_by='left')
53
54 class Comment(Entity):
55 time = Field(DateTime, default=datetime.datetime.now)
56 text = Field(Unicode(65536))
57
58 # Comments are a tree, and are stored as a nested set, because:
59 # - It's easy to get a subtree with a single query.
60 # - It's easy to get a comment's depth.
61 # - It's easy to sort comments without recursion.
62 # The only real disadvantage is that adding a comment requires a quick
63 # update of all the following comments (in post-order), but that's rare
64 # enough that it shouldn't be a problem.
65 left = Field(Integer, index=True)
66 right = Field(Integer)
67
68 discussion = ManyToOne('Discussion')
69 user = ManyToOne('User')
70
71 def __init__(self, parent=None, **kwargs):
72 """Constructor override to set left/right correctly on a new comment.
73 """
74 super(Comment, self).__init__(**kwargs)
75
76 # Keep the comment count updated
77 self.discussion.count += 1
78
79 if parent:
80 # Parent comment given.
81 parent_right = parent.right
82 # Shift every left/right ahead by two to make room for the new
83 # comment's left/right
84 Comment.query.filter(Comment.discussion == self.discussion) \
85 .filter(Comment.left >= parent_right) \
86 .update({ Comment.left: Comment.left + 2 })
87 Comment.query.filter(Comment.discussion == self.discussion) \
88 .filter(Comment.right >= parent_right) \
89 .update({ Comment.right: Comment.right + 2 })
90
91 # Then stick the new comment in the right place
92 self.left = parent_right
93 self.right = parent_right + 1
94
95 else:
96 query = session.query(func.max(Comment.right)) \
97 .filter(Comment.discussion == self.discussion)
98 (max_right,) = query.one()
99
100 if not max_right:
101 # No comments yet. Use 1 and 2
102 self.left = 1
103 self.right = 2
104 else:
105 self.left = max_right + 1
106 self.right = max_right + 2
107
108 @property
109 def parent(self):
110 """Returns this comment's parent. This is cached, hence its being a
111 property and not a method.
112 """
113 if not hasattr(self, '_parent'):
114 self._parent = Comment.query \
115 .filter(Comment.discussion_id == self.discussion_id) \
116 .filter(Comment.left < self.left) \
117 .filter(Comment.right > self.right) \
118 .order_by(Comment.left.desc()) \
119 .first()
120 return self._parent