Comment threads are indented correctly.
Viewing a comment subthread works.
Replying to an arbitrarily deeply-nested comment works.
Creating a nested comment updates the rest of the discussion correctly.
# Comments
with map.submapper(controller='comments') as sub:
sub.connect('/*owner_url/comments', action='thread')
+ sub.connect('/*owner_url/comments/{id}', action='thread')
sub.connect('/*owner_url/comments/reply', action='reply')
+ sub.connect('/*owner_url/comments/{id}/reply', action='reply')
sub.connect('/*owner_url/comments/reply_done', action='reply_done', **require_POST)
+ sub.connect('/*owner_url/comments/{id}/reply_done', action='reply_done', **require_POST)
with map.submapper(controller="art") as sub:
sub.connect('new_art', '/art/new', action="new")
import elixir
from pylons import config, request, response, session, tmpl_context as c
from pylons.controllers.util import abort, redirect, redirect_to
+from sqlalchemy import and_
from floof.lib.base import BaseController, render
from floof.model.art import Art
class CommentsController(BaseController):
- def thread(self, owner_url):
+ def thread(self, owner_url, id=None):
"""View a thread of comments, either attached to an item or starting
from a parent comment belonging to that item.
"""
owner_object = find_owner(owner_url)
- c.comments = owner_object.discussion.comments
+ c.owner_url = owner_url
+ c.root_comment_id = id
+
+ if id:
+ # Get a thread starting from a certain point
+ c.root_comment = Comment.query.get(id)
+ if c.root_comment.discussion != owner_object.discussion:
+ abort(404)
+
+ c.comments = Comment.query.filter(and_(
+ Comment.discussion_id == owner_object.discussion_id,
+ Comment.left > c.root_comment.left,
+ Comment.right < c.root_comment.right
+ )).all()
+ else:
+ # Get everything
+ c.root_comment = None
+ c.comments = owner_object.discussion.comments
+
return render('/comments/thread.mako')
- def reply(self, owner_url):
+ def reply(self, owner_url, id=None):
"""Reply to a comment or discussion."""
+
+ c.parent_comment_id = id
+ if id:
+ c.parent_comment = Comment.query.get(id)
+ else:
+ c.parent_comment = None
+
return render('/comments/reply.mako')
- def reply_done(self, owner_url):
+ def reply_done(self, owner_url, id=None):
"""Finish replying to a comment or discussion."""
# XXX form validation woo
owner_object = find_owner(owner_url)
+ if id:
+ parent_comment = Comment.query.get(id)
+ else:
+ parent_comment = None
+
new_comment = Comment(
text=request.params['text'],
user=c.user,
discussion=owner_object.discussion,
+ parent=parent_comment,
)
elixir.session.commit()
"""Given a view route, returns the owner_url route parameter for generating
comment URLs.
"""
+ if 'owner_url' in route:
+ # We're already in a comments page. Reuse the existing owner URL
+ return route['owner_url']
+
# url() returns URLs beginning with a slash. We just need to strip it.
return url(**route)[1:]
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.
+ """
+
+ 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)
self.discussion.count += 1
if parent:
- # Parent comment given. Add this comment just before the parent's
- # right side...
- self.left = parent.right
- self.right = parent.right + 1
-
- # ...then adjust all rightward comments accordingly
+ # 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({ Comment.left: Comment.left + 2 })
Comment.query.filter(Comment.discussion == self.discussion) \
- .filter(Comment.right > parent.right) \
- .update({ Comment.left: Comment.left + 2,
- Comment.right: Comment.right + 2 })
+ .filter(Comment.right >= parent_right) \
+ .update({ Comment.right: Comment.right + 2 })
- # And, finally, update the parent's right endpoint
- parent.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)) \
else:
self.left = max_right + 1
self.right = max_right + 2
+
.artwork-grid li {display:inline;}
/*** Common bits and pieces ***/
-/* General form layout */
a {color:blue; text-decoration:none; pointer:cursor;} /* Who needs visited links */
+p { margin: 0.25em 0 1em 0; }
+
+/* General form layout */
dl.form { margin: 1em 0; padding-left: 1em; border-left: 0.5em solid gray; }
dl.form dt { padding-bottom: 0.25em; font-style: italic; }
dl.form dd { margin-bottom: 0.5em; }
+/* Comments */
+.comment {}
+.comment .header { background: #d8d8d8; }
+
/*** Individual page layout ***/
<%def name="comment_block(comments)">
<h1>${len(comments)} comments</h1>
-## XXX make sure these do the right thing when this is a subtree
<p><a href="${url(controller='comments', action='thread', owner_url=h.get_comment_owner_url(**c.route))}">View all</a></p>
<p><a href="${url(controller='comments', action='reply', owner_url=h.get_comment_owner_url(**c.route))}">Reply</a></p>
${comment_thread(comments)}
</%def>
<%def name="comment_thread(comments)">
-% for comment in comments:
-<div class="comment">
- <div class="user">${comment.user.name}</div>
- <div class="time">${comment.time}</div>
+<%! from floof.model.comments import indent_comments %>\
+% for comment in indent_comments(comments):
+${single_comment(comment)}
+% endfor
+</%def>
+
+<%def name="single_comment(comment)">
+<div class="comment" style="margin-left: ${comment.indent}em;">
+ <div class="header">
+ <div class="user">${comment.user.name}</div>
+ <div class="time">${comment.time}</div>
+ <div class="link"><a href="${url(controller='comments', action='thread', id=comment.id, owner_url=h.get_comment_owner_url(**c.route))}">Link</a></div>
+ </div>
<p>${comment.text}</p>
</div>
-% endfor
</%def>
<%namespace name="comments" file="/comments/lib.mako" />
<h1>Reply</h1>
+% if c.parent_comment:
+${h.form(url(controller='comments', action='reply_done', id=c.parent_comment_id, owner_url=c.owner_url), method='post')}
+<p>Replying to comment ${c.parent_comment_id}</p>
+% else:
${h.form(url(controller='comments', action='reply_done', owner_url=c.owner_url), method='post')}
+% endif
<dl class="form">
<dt>Comment</dt>
<dd>${h.textarea('text')}</dd>
<%inherit file="/base.mako" />
<%namespace name="comments" file="/comments/lib.mako" />
-${comments.comment_block(c.comments)}
+% if c.root_comment:
+${comments.single_comment(c.root_comment)}
+% endif
+
+<h1>${len(c.comments)} comments</h1>
+<p><a href="${url(controller='comments', action='thread', owner_url=c.owner_url)}">View all</a></p>
+% if c.root_comment_id:
+<p><a href="${url(controller='comments', action='reply', id=c.root_comment_id, owner_url=c.owner_url)}">Reply</a></p>
+% else:
+<p><a href="${url(controller='comments', action='reply', owner_url=c.owner_url)}">Reply</a></p>
+% endif
+${comments.comment_thread(c.comments)}