diff --git a/sphinx/websupport/storage/db.py b/sphinx/websupport/storage/db.py index 983eb66d9..64f7f3e25 100644 --- a/sphinx/websupport/storage/db.py +++ b/sphinx/websupport/storage/db.py @@ -34,6 +34,12 @@ class Node(Base): source = Column(Text, nullable=False) def nested_comments(self, username, moderator): + """Create a tree of comments. First get all comments that are + descendents of this node, then convert them to a tree form. + + :param username: the name of the user to get comments for. + :param moderator: whether the user is moderator. + """ session = Session() if username: @@ -45,24 +51,30 @@ class Node(Base): cvalias = aliased(CommentVote, sq) q = session.query(Comment, cvalias.value).outerjoin(cvalias) else: + # If a username is not provided, we don't need to join with + # CommentVote. q = session.query(Comment) # Filter out all comments not descending from this node. q = q.filter(Comment.path.like(str(self.id) + '.%')) - # Filter out non-displayed comments if this isn't a moderator. + if not moderator: q = q.filter(Comment.displayed == True) + # Retrieve all results. Results must be ordered by Comment.path # so that we can easily transform them from a flat list to a tree. results = q.order_by(Comment.path).all() session.close() - # We now need to convert the flat list of results to a nested - # lists to form the comment tree. Results will by ordered by - # the materialized path. return self._nest_comments(results, username) def _nest_comments(self, results, username): + """Given the flat list of results, convert the list into a + tree. + + :param results: the flat list of comments + :param username: the name of the user requesting the comments. + """ comments = [] list_stack = [comments] for r in results: @@ -87,6 +99,7 @@ class Node(Base): self.source = source class Comment(Base): + """An individual Comment being stored.""" __tablename__ = db_prefix + 'comments' id = Column(Integer, primary_key=True) @@ -110,6 +123,9 @@ class Comment(Base): self.proposal_diff = proposal_diff def set_path(self, node_id, parent_id): + """Set the materialized path for this comment.""" + # This exists because the path can't be set until the session has + # been flushed and this Comment has an id. if node_id: self.path = '%s.%s' % (node_id, self.id) else: @@ -120,6 +136,9 @@ class Comment(Base): self.path = '%s.%s' % (parent_path, self.id) def serializable(self, vote=0): + """Creates a serializable representation of the comment. This is + converted to JSON, and used on the client side. + """ delta = datetime.now() - self.time time = {'year': self.time.year, @@ -149,6 +168,9 @@ class Comment(Base): 'children': []} def pretty_delta(self, delta): + """Create a pretty representation of the Comment's age. + (e.g. 2 minutes). + """ days = delta.days seconds = delta.seconds hours = seconds / 3600 @@ -162,6 +184,7 @@ class Comment(Base): return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt class CommentVote(Base): + """A vote a user has made on a Comment.""" __tablename__ = db_prefix + 'commentvote' username = Column(String(64), primary_key=True) diff --git a/sphinx/websupport/storage/differ.py b/sphinx/websupport/storage/differ.py index c82ba7427..068d7e6fc 100644 --- a/sphinx/websupport/storage/differ.py +++ b/sphinx/websupport/storage/differ.py @@ -14,39 +14,18 @@ from cgi import escape from difflib import Differ class CombinedHtmlDiff(object): - + """Create an HTML representation of the differences between two pieces + of text. + """ highlight_regex = re.compile(r'([\+\-\^]+)') - def _highlight_text(self, text, next, tag): - next = next[2:] - new_text = [] - start = 0 - for match in self.highlight_regex.finditer(next): - new_text.append(text[start:match.start()]) - new_text.append('<%s>' % tag) - new_text.append(text[match.start():match.end()]) - new_text.append('' % tag) - start = match.end() - new_text.append(text[start:]) - return ''.join(new_text) - - def _handle_line(self, line, next=None): - prefix = line[0] - text = line[2:] - - if prefix == ' ': - return text - elif prefix == '?': - return '' - - if next is not None and next[0] == '?': - tag = 'ins' if prefix == '+' else 'del' - text = self._highlight_text(text, next, tag) - css_class = 'prop_added' if prefix == '+' else 'prop_removed' - - return '%s\n' % (css_class, text.rstrip()) - def make_html(self, source, proposal): + """Return the HTML representation of the differences between + `source` and `proposal`. + + :param source: the original text + :param proposal: the proposed text + """ proposal = escape(proposal) differ = Differ() @@ -64,3 +43,37 @@ class CombinedHtmlDiff(object): self._handle_line(line) break return ''.join(html) + + def _handle_line(self, line, next=None): + """Handle an individual line in a diff.""" + prefix = line[0] + text = line[2:] + + if prefix == ' ': + return text + elif prefix == '?': + return '' + + if next is not None and next[0] == '?': + tag = 'ins' if prefix == '+' else 'del' + text = self._highlight_text(text, next, tag) + css_class = 'prop_added' if prefix == '+' else 'prop_removed' + + return '%s\n' % (css_class, text.rstrip()) + + def _highlight_text(self, text, next, tag): + """Highlight the specific changes made to a line by adding + and tags. + """ + next = next[2:] + new_text = [] + start = 0 + for match in self.highlight_regex.finditer(next): + new_text.append(text[start:match.start()]) + new_text.append('<%s>' % tag) + new_text.append(text[match.start():match.end()]) + new_text.append('' % tag) + start = match.end() + new_text.append(text[start:]) + return ''.join(new_text) + diff --git a/sphinx/websupport/storage/sqlalchemystorage.py b/sphinx/websupport/storage/sqlalchemystorage.py index 7a906dcba..553450d32 100644 --- a/sphinx/websupport/storage/sqlalchemystorage.py +++ b/sphinx/websupport/storage/sqlalchemystorage.py @@ -18,6 +18,9 @@ from sphinx.websupport.storage.db import Base, Node, Comment, CommentVote,\ from sphinx.websupport.storage.differ import CombinedHtmlDiff class SQLAlchemyStorage(StorageBackend): + """A :class:`~sphinx.websupport.storage.StorageBackend` using + SQLAlchemy. + """ def __init__(self, engine): self.engine = engine Base.metadata.bind = engine @@ -40,7 +43,8 @@ class SQLAlchemyStorage(StorageBackend): def add_comment(self, text, displayed, username, time, proposal, node_id, parent_id, moderator): session = Session() - + proposal_diff = None + if node_id and proposal: node = session.query(Node).filter(Node.id == node_id).one() differ = CombinedHtmlDiff() @@ -51,19 +55,18 @@ class SQLAlchemyStorage(StorageBackend): if not parent.displayed: raise CommentNotAllowedError( "Can't add child to a parent that is not displayed") - proposal_diff = None - else: - proposal_diff = None comment = Comment(text, displayed, username, 0, time or datetime.now(), proposal, proposal_diff) session.add(comment) session.flush() + # We have to flush the session before setting the path so the + # Comment has an id. comment.set_path(node_id, parent_id) session.commit() - comment = comment.serializable() + d = comment.serializable() session.close() - return comment + return d def delete_comment(self, comment_id, username, moderator): session = Session() @@ -72,6 +75,7 @@ class SQLAlchemyStorage(StorageBackend): if moderator or comment.username == username: comment.username = '[deleted]' comment.text = '[deleted]' + comment.proposal = '' session.commit() session.close() else: