From 03056a8a31062034edb88521cafb2fa66238076f Mon Sep 17 00:00:00 2001 From: Jacob Mason Date: Thu, 8 Jul 2010 21:35:19 -0500 Subject: [PATCH] Basic comment system. --- sphinx/builders/websupport.py | 2 +- sphinx/websupport/api.py | 37 ++++++++- sphinx/websupport/comments/__init__.py | 106 +++++++++++++++++++++++++ sphinx/websupport/comments/db.py | 53 +++++++++++++ sphinx/writers/websupport.py | 16 ++-- 5 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 sphinx/websupport/comments/__init__.py create mode 100644 sphinx/websupport/comments/db.py diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index ddf525c4b..e2caeccd1 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -28,7 +28,7 @@ class WebSupportBuilder(StandaloneHTMLBuilder): def write_doc(self, docname, doctree): # The translator needs the docname to generate ids. - self.docname = docname + self.cur_docname = docname StandaloneHTMLBuilder.write_doc(self, docname, doctree) def get_target_uri(self, docname, typ=None): diff --git a/sphinx/websupport/api.py b/sphinx/websupport/api.py index 37f590971..b8274f52c 100644 --- a/sphinx/websupport/api.py +++ b/sphinx/websupport/api.py @@ -11,26 +11,46 @@ import cPickle as pickle from os import path +from datetime import datetime from jinja2 import Environment, FileSystemLoader from sphinx.application import Sphinx +from sphinx.util.osutil import ensuredir from sphinx.websupport.search import search_adapters +from sphinx.websupport import comments as sphinxcomments class WebSupportApp(Sphinx): def __init__(self, *args, **kwargs): self.search = kwargs.pop('search', None) + self.comments = kwargs.pop('comments', None) Sphinx.__init__(self, *args, **kwargs) class WebSupport(object): - def __init__(self, srcdir='', outdir='', search=None): + def __init__(self, srcdir='', outdir='', search=None, + comments=None): self.srcdir = srcdir self.outdir = outdir or path.join(self.srcdir, '_build', 'websupport') - self.init_templating() + self.init_templating() if search is not None: self.init_search(search) + self.init_comments(comments) + + def init_comments(self, comments): + if isinstance(comments, sphinxcomments.CommentBackend): + self.comments = comments + elif comments is not None: + # If a CommentBackend isn't provided, use the default + # SQLAlchemy backend with an SQLite db. + from sphinx.websupport.comments import SQLAlchemyComments + from sqlalchemy import create_engine + db_path = path.join(self.outdir, 'comments', 'comments.db') + ensuredir(path.dirname(db_path)) + engine = create_engine('sqlite:///%s' % db_path) + self.comments = SQLAlchemyComments(engine) + def init_templating(self): import sphinx template_path = path.join(path.dirname(sphinx.__file__), @@ -52,8 +72,11 @@ class WebSupport(object): path.join(self.outdir, 'doctrees')) app = WebSupportApp(self.srcdir, self.srcdir, self.outdir, doctreedir, 'websupport', - search=self.search) + search=self.search, + comments=self.comments) + self.comments.pre_build() app.build() + self.comments.post_build() def get_document(self, docname): infilename = path.join(self.outdir, docname + '.fpickle') @@ -70,3 +93,11 @@ class WebSupport(object): document['body'] = self.results_template.render(ctx) document['title'] = 'Search Results' return document + + def get_comments(self, node_id): + return self.comments.get_comments(node_id) + + def add_comment(self, parent_id, text, displayed=True, user_id=None, + rating=0, time=None): + return self.comments.add_comment(parent_id, text, displayed, user_id, + rating, time) diff --git a/sphinx/websupport/comments/__init__.py b/sphinx/websupport/comments/__init__.py new file mode 100644 index 000000000..1876d4343 --- /dev/null +++ b/sphinx/websupport/comments/__init__.py @@ -0,0 +1,106 @@ +from datetime import datetime + +from sqlalchemy.orm import sessionmaker + +from sphinx.websupport.comments.db import Base, Node, Comment + +Session = sessionmaker() + +class CommentBackend(object): + def pre_build(self): + pass + + def add_node(self, document, line, source, treeloc): + raise NotImplemented + + def post_build(self): + pass + + def add_comment(self, parent_id, text, displayed, user_id, rating, time): + raise NotImplemented + + def get_comments(self, parent_id): + raise NotImplemented + + +class SQLAlchemyComments(CommentBackend): + def __init__(self, engine): + self.engine = engine + Base.metadata.bind = engine + Base.metadata.create_all() + Session.configure(bind=engine) + self.session = Session() + + def add_node(self, document, line, source, treeloc): + node = Node(document, line, source, treeloc) + self.session.add(node) + return node.id + + def post_build(self): + self.session.commit() + + def add_comment(self, parent_id, text, displayed, user_id, rating, time): + time = time or datetime.now() + + id = parent_id[1:] + if parent_id[0] == 's': + node = self.session.query(Node).filter(Node.id == id).first() + comment = Comment(text, displayed, user_id, rating, + time, node=node) + elif parent_id[0] == 'c': + parent = self.session.query(Comment).filter(Comment.id == id).first() + comment = Comment(text, displayed, user_id, rating, + time, parent=parent) + + self.session.add(comment) + self.session.commit() + return self.serializable(comment) + + def get_comments(self, parent_id): + parent_id = parent_id[1:] + node = self.session.query(Node).filter(Node.id == parent_id).first() + comments = [] + for comment in node.comments: + comments.append(self.serializable(comment)) + + return comments + + def serializable(self, comment): + time = {'year': comment.time.year, + 'month': comment.time.month, + 'day': comment.time.day, + 'hour': comment.time.hour, + 'minute': comment.time.minute, + 'second': comment.time.second, + 'iso': comment.time.isoformat(), + 'delta': self.pretty_delta(comment)} + + return {'text': comment.text, + 'user_id': comment.user_id, + 'id': comment.id, + 'rating': self.pretty_rating(comment), + 'time': time, + 'node': comment.node.id if comment.node else None, + 'parent': comment.parent.id if comment.parent else None, + 'children': [self.serializable(child) + for child in comment.children]} + + def pretty_rating(self, comment): + if comment.rating == 1: + return '%s point' % comment.rating + else: + return '%s points' % comment.rating + + def pretty_delta(self, comment): + delta = datetime.now() - comment.time + days = delta.days + seconds = delta.seconds + hours = seconds / 3600 + minutes = seconds / 60 + + if days == 0: + dt = (minutes, 'minute') if hours == 0 else (hours, 'hour') + else: + dt = (days, 'day') + + return '%s %s ago' % dt if dt[0] == 1 else '%s %ss ago' % dt diff --git a/sphinx/websupport/comments/db.py b/sphinx/websupport/comments/db.py new file mode 100644 index 000000000..ff731a9f1 --- /dev/null +++ b/sphinx/websupport/comments/db.py @@ -0,0 +1,53 @@ +from sqlalchemy import Column, Integer, Text, String, Boolean, ForeignKey,\ +DateTime +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relation + +Base = declarative_base() + +db_prefix = 'sphinx_' + +class Node(Base): + """Data about a Node in a doctree.""" + __tablename__ = db_prefix + 'nodes' + + id = Column(Integer, primary_key=True) + document = Column(String(256), nullable=False) + line = Column(Integer) + source = Column(Text, nullable=False) + treeloc = Column(String(32), nullable=False) + + def __init__(self, document, line, source, treeloc): + self.document = document + self.line = line + self.source = source + self.treeloc = treeloc + + def __repr__(self): + return '' % (document, treeloc) + +class Comment(Base): + __tablename__ = db_prefix + 'comments' + + id = Column(Integer, primary_key=True) + rating = Column(Integer, nullable=False) + time = Column(DateTime, nullable=False) + text = Column(Text, nullable=False) + displayed = Column(Boolean, index=True, default=False) + user_id = Column(String(50), nullable=True) + + node_id = Column(Integer, ForeignKey(db_prefix + 'nodes.id')) + node = relation(Node, backref='comments') + + parent_id = Column(Integer, ForeignKey(db_prefix + 'comments.id')) + parent = relation('Comment', backref='children', remote_side=[id]) + + def __init__(self, text, displayed, user_id, rating, time, + node=None, parent=None): + self.text = text + self.displayed = displayed + self.user_id = user_id + self.rating = rating + self.time = time + self.node = node + self.parent = parent diff --git a/sphinx/writers/websupport.py b/sphinx/writers/websupport.py index 4afc3ecbd..18c0807d6 100644 --- a/sphinx/writers/websupport.py +++ b/sphinx/writers/websupport.py @@ -41,14 +41,14 @@ class WebSupportTranslator(HTMLTranslator): # node will not be commented. if not self.in_commentable: self.in_commentable = True - id = self.create_id(node) + node_id = self.add_db_node(node) # We will place the node in the HTML id attribute. If the node - # already has another id (for indexing purposes) put an empty + # already has an id (for indexing purposes) put an empty # span with the existing id directly before this node's HTML. if node.attributes['ids']: self.body.append('' % node.attributes['ids'][0]) - node.attributes['ids'] = [id] + node.attributes['ids'] = ['s%s' % node_id] node.attributes['classes'].append(self.comment_class) def handle_depart_commentable(self, node): @@ -56,6 +56,10 @@ class WebSupportTranslator(HTMLTranslator): if self.comment_class in node.attributes['classes']: self.in_commentable = False - def create_id(self, node): - self.current_id += 1 - return '%s_%s' % (node.__class__.__name__, self.current_id) + def add_db_node(self, node): + comments = self.builder.app.comments + db_node_id = comments.add_node(document=self.builder.cur_docname, + line=node.line, + source=node.rawsource, + treeloc='???') + return db_node_id