From dce049a4e2dd5fee51fe9d8ebafd4c9401c0c5dd Mon Sep 17 00:00:00 2001 From: Eevee Date: Mon, 19 Oct 2009 20:13:12 -0700 Subject: [PATCH 1/1] More thorough support for comment threading. 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. --- floof/config/routing.py | 3 ++ floof/controllers/comments.py | 40 +++++++++++++++++++--- floof/lib/helpers.py | 4 +++ floof/model/comments.py | 65 ++++++++++++++++++++++++++++++------ floof/public/layout.css | 8 ++++- floof/templates/comments/lib.mako | 19 +++++++---- floof/templates/comments/reply.mako | 5 +++ floof/templates/comments/thread.mako | 13 +++++++- 8 files changed, 134 insertions(+), 23 deletions(-) diff --git a/floof/config/routing.py b/floof/config/routing.py index 5f729e6..603afd0 100644 --- a/floof/config/routing.py +++ b/floof/config/routing.py @@ -49,8 +49,11 @@ def make_map(): # 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") diff --git a/floof/controllers/comments.py b/floof/controllers/comments.py index 70c9073..b096ba2 100644 --- a/floof/controllers/comments.py +++ b/floof/controllers/comments.py @@ -3,6 +3,7 @@ import logging 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 @@ -33,28 +34,59 @@ def find_owner(owner_url): 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() diff --git a/floof/lib/helpers.py b/floof/lib/helpers.py index 1c2f0c1..b4cac1f 100644 --- a/floof/lib/helpers.py +++ b/floof/lib/helpers.py @@ -35,5 +35,9 @@ def get_comment_owner_url(**route): """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:] diff --git a/floof/model/comments.py b/floof/model/comments.py index e1ad150..bcfee9f 100644 --- a/floof/model/comments.py +++ b/floof/model/comments.py @@ -3,6 +3,47 @@ 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. + """ + + 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) @@ -34,19 +75,20 @@ class Comment(Entity): 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)) \ @@ -60,3 +102,4 @@ class Comment(Entity): else: self.left = max_right + 1 self.right = max_right + 2 + diff --git a/floof/public/layout.css b/floof/public/layout.css index a098b5e..b175705 100644 --- a/floof/public/layout.css +++ b/floof/public/layout.css @@ -14,13 +14,19 @@ body { font-family: sans-serif; font-size: 12px; } .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 ***/ diff --git a/floof/templates/comments/lib.mako b/floof/templates/comments/lib.mako index 38e6534..0c8ebfb 100644 --- a/floof/templates/comments/lib.mako +++ b/floof/templates/comments/lib.mako @@ -1,17 +1,24 @@ <%def name="comment_block(comments)">

${len(comments)} comments

-## XXX make sure these do the right thing when this is a subtree

View all

Reply

${comment_thread(comments)} <%def name="comment_thread(comments)"> -% for comment in comments: -
-
${comment.user.name}
-
${comment.time}
+<%! from floof.model.comments import indent_comments %>\ +% for comment in indent_comments(comments): +${single_comment(comment)} +% endfor + + +<%def name="single_comment(comment)"> +
+
+
${comment.user.name}
+
${comment.time}
+ +

${comment.text}

-% endfor diff --git a/floof/templates/comments/reply.mako b/floof/templates/comments/reply.mako index 9941433..916cc91 100644 --- a/floof/templates/comments/reply.mako +++ b/floof/templates/comments/reply.mako @@ -2,7 +2,12 @@ <%namespace name="comments" file="/comments/lib.mako" />

Reply

+% if c.parent_comment: +${h.form(url(controller='comments', action='reply_done', id=c.parent_comment_id, owner_url=c.owner_url), method='post')} +

Replying to comment ${c.parent_comment_id}

+% else: ${h.form(url(controller='comments', action='reply_done', owner_url=c.owner_url), method='post')} +% endif
Comment
${h.textarea('text')}
diff --git a/floof/templates/comments/thread.mako b/floof/templates/comments/thread.mako index 7b3580a..359c296 100644 --- a/floof/templates/comments/thread.mako +++ b/floof/templates/comments/thread.mako @@ -1,4 +1,15 @@ <%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 + +

${len(c.comments)} comments

+

View all

+% if c.root_comment_id: +

Reply

+% else: +

Reply

+% endif +${comments.comment_thread(c.comments)} -- 2.7.4