Lots of documentation and a few small API changes

This commit is contained in:
Jacob Mason
2010-07-12 01:41:39 -05:00
parent a1c99c9603
commit cf06e98ebd
4 changed files with 346 additions and 223 deletions

View File

@@ -1,22 +1,32 @@
.. _websupportapi:
Web Support API
===============
.. currentmodule:: sphinx.websupport
The WebSupport Class
====================
.. module:: sphinx.websupport.api
.. class:: WebSupport
The :class:`WebSupport` class provides a central interface for
working with Sphinx documentation.
The main API class for the web support package. All interactions
with the web support package should occur through this class.
.. method:: init(srcdir='', outdir='')
:param srcdir: the directory containing the reStructuredText files
:param outdir: the directory in which to place the built data
:param search: the search system to use
:param comments: an instance of a CommentBackend
Methods
~~~~~~~
Initialize attributes.
.. automethod:: sphinx.websupport.WebSupport.build
.. method:: build()
.. automethod:: sphinx.websupport.WebSupport.get_document
Build the data used by the web support package.
.. automethod:: sphinx.websupport.WebSupport.get_comments
.. method:: get_document(docname)
.. automethod:: sphinx.websupport.WebSupport.add_comment
.. automethod:: sphinx.websupport.WebSupport.process_vote
.. automethod:: sphinx.websupport.WebSupport.get_search_results
Retrieve the context dictionary corresponding to the *docname*.

View File

@@ -3,118 +3,215 @@
Web Support Quick Start
=======================
Getting Started
~~~~~~~~~~~~~~~
To use the :ref:`websupportapi` in your application you must import
the :class:`~sphinx.websupport.api.WebSupport` object::
from sphinx.websupport import support
This provides a reference to a :class:`~sphinx.websupport.api.WebSupport`
object. You will then need to provide some information about your
environment::
support.init(srcdir='/path/to/rst/sources/',
outdir='/path/to/build/outdir',
search='xapian')
Note: You only need to provide a srcdir if you are building documentation.
Building Documentation Data
~~~~~~~~~~~~~~~~~~~~~~~~~~~
In order to use the web support package in a webapp, you will need to
build the data it uses. This data includes document data used to display
documentation and search indexes. To build this data, call the build method::
To make use of the web support package in your application you will
need to build that data it uses. This data includes pickle files representing
documents, search indices, and node data that is used to track where
comments and other things are in a document. Do do this you will need
to create an instance of the :class:`~sphinx.websupport.api.WebSupport`
class and call it's :meth:`~sphinx.websupport.WebSupport.build` method::
from sphinx.websupport import WebSupport
support = WebSupport(srcdir='/path/to/rst/sources/',
outdir='/path/to/build/outdir',
search='xapian')
support.build()
This will create the data the web support package needs and place
it in *outdir*.
This will read reStructuredText sources from `srcdir` and place the
necessary data in `outdir`. This directory contains all the data needed
to display documents, search through documents, and add comments to
documents.
Accessing Document Data
~~~~~~~~~~~~~~~~~~~~~~~
Integrating Sphinx Documents Into Your Webapp
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To access document data, call the get_document(docname) method. For example,
to retrieve the "contents" document, do this::
contents_doc = support.get_document('contents')
This will return a dictionary containing the context you need to render
a document.
Performing Searches
~~~~~~~~~~~~~~~~~~~
To perform a search, call the get_search_results(q) method, with *q* being
the string to be searched for::
q = request.GET['q']
search_doc = support.get_search_results(q)
This will return a dictionary in the same format as get_document() returns.
Full Example
~~~~~~~~~~~~
A more useful example, in the form of a `Flask <http://flask.pocoo.org/>`_
application is::
from flask import Flask, render_template
from sphinx.websupport import support
app = Flask(__name__)
support.init(outdir='/path/to/sphinx/data')
Now that you have the data, it's time to use it for something useful.
Start off by creating a :class:`~sphinx.websupport.WebSupport` object
for your application::
@app.route('/docs/<path:docname>')
from sphinx.websupport import WebSupport
support = WebSupport(datadir='/path/to/the/data',
search='xapian')
You'll only need one of these for each set of documentation you will be
working with. You can then call it's
:meth:`~sphinx.websupport.WebSupport.get_document` method to access
individual documents.
contents = support.get_document('contents')
This will return a dictionary containing the following items:
* **body**: The main body of the document as HTML
* **sidebar**: The sidebar of the document as HTML
* **relbar**: A div containing links to related documents
* **title**: The title of the document
This dict can then be used as context for templates. The goal is to be
easy to integrate with your existing templating system. An example using
`Jinja2 <http://jinja.pocoo.org/2/>`_ is:
.. sourcecode:: html+jinja
{%- extends "layout.html" %}
{%- block title %}
{{ document.title }}
{%- endblock %}
{%- block relbar %}
{{ document.relbar|safe }}
{%- endblock %}
{%- block body %}
{{ document.body|safe }}
{%- endblock %}
{%- block sidebar %}
{{ document.sidebar|safe }}
{%- endblock %}
Most likely you'll want to create one function that can handle all of
document requests. An example `Flask <http://flask.pocoo.org/>`_ function
that performs this is::
@app.route('/<path:docname>')
def doc(docname):
document = support.get_document(docname)
return render_template('doc.html', document=document)
@app.route('/docs/search')
This captures the request path, and passes it directly to
:meth:`~sphinx.websupport.WebSupport.get_document`, which retrieves
the correct document.
.. note::
This only works works if your documentation is served from your
document root. If it is served from another directory, you will
need to prefix the url route with that directory::
@app.route('/docs/<path:docname>')
Performing Searches
~~~~~~~~~~~~~~~~~~~
To use the search form built-in to the Sphinx sidebar, create a function
to handle requests to the url 'search' relative to the documentation root.
The user's search query will be in the GET parameters, with the key `q`.
Then use the :meth:`~sphinx.websupport.WebSupport.get_search_results` method
to retrieve search results. In `Flask <http://flask.pocoo.org/>`_ that
would be like this::
@app.route('/search')
def search():
document = support.get_search_results(request.args.get('q', ''))
return render_template('doc.html', document=document)
q = request.args.get('q')
document = support.get_search_results(q)
return render_template('doc.html', document=document)
In the previous example the doc.html template would look something
like this::
Note that we used the same template to render our search results as we
did to render our documents. That's because
:meth:`~sphinx.websupport.WebSupport.get_search_results` returns a context
dict in the same format as
:meth:`~sphinx.websupport.WebSupport.get_document`.
{% extends "base.html" %}
Comments
~~~~~~~~
{% block title %}
{{ document.title }}
{% endblock %}
The web support package provides a way to attach comments to some nodes
in your document. It marks these nodes by adding a class and id to these
nodes. A client side script can then locate these nodes, and manipulate
them to allow commenting. A jquery script is also being developed that will
be included when it's complete. For now you can find the script here:
`websupport.js <http://bit.ly/cyaRaF>`_.
{% block extra_js %}
<script type="text/javascript" src="/static/jquery.js"></script>
<script type="text/javascript">
<!--
$(document).ready(function() {
$(".spxcmt").append(' <a href="#" class="sphinx_comment"><img src="/static/comment.png" /></a>');
$("a.sphinx_comment").click(function() {
id = $(this).parent().attr('id');
alert('[ comment stub ' + id + ' ]');
return false;
});
});
-->
</script>
{% endblock %}
If you use the script that is included, you will have to define some
simple templates that the script uses to display comments. The first
template defines the layout for the popup div used to display comments:
{% block relbar %}
{{ document.relbar|safe }}
{% endblock %}
.. sourcecode:: guess
{% block body %}
{{ document.body|safe }}
{% endblock %}
<script type="text/html" id="popup_template">
<div class="popup_comment">
<a id="comment_close" href="#">x</a>
<h1>Comments</h1>
<form method="post" id="comment_form" action="/docs/add_comment">
<textarea name="comment"></textarea>
<input type="submit" value="add comment" id="comment_button" />
<input type="hidden" name="parent" />
<p class="sort_options">
Sort by:
<a href="#" class="sort_option" id="rating">top</a>
<a href="#" class="sort_option" id="ascage">newest</a>
<a href="#" class="sort_option" id="age">oldest</a>
</p>
</form>
<h3 id="comment_notification">loading comments... <img src="/static/ajax-loader.gif" alt="" /></h3>
<ul id="comment_ul"></ul>
</div>
<div id="focuser"></div>
</script>
{% block sidebar %}
{{ document.sidebar|safe }}
{% endblock %}
The next templat is an `li` that contains the form used to
reply to a comment:
.. sourcecode:: guess
<script type="text/html" id="reply_template">
<li>
<div class="reply_div" id="rd<%id%>">
<form id="rf<%id%>">
<textarea name="comment"></textarea>
<input type="submit" value="add reply" />
<input type="hidden" name="parent" value="c<%id%>" />
</form>
</div>
</li>
</script>
The final template contains HTML that will be used to display comments
in the comment tree:
.. sourcecode:: guess
<script type="text/html" id="comment_template">
<div id="cd<%id%>" class="spxcdiv">
<div class="vote">
<div class="arrow">
<a href="#" id="uv<%id%>" class="vote">
<img src="<%upArrow%>" />
</a>
<a href="#" id="uu<%id%>" class="un vote">
<img src="<%upArrowPressed%>" />
</a>
</div>
<div class="arrow">
<a href="#" id="dv<%id%>" class="vote">
<img src="<%downArrow%>" id="da<%id%>" />
</a>
<a href="#" id="du<%id%>" class="un vote">
<img src="<%downArrowPressed%>" />
</a>
</div>
</div>
<div class="comment_content">
<p class="tagline comment">
<span class="user_id"><%username%></span>
<span class="rating"><%pretty_rating%></span>
<span class="delta"><%time.delta%></span>
</p>
<p class="comment_text comment"><%text%></p>
<p class="comment_opts comment">
<a href="#" class="reply" id="rl<%id%>">reply</a>
<a href="#" class="close_reply" id="cr<%id%>">hide</a>
</p>
<ul class="children" id="cl<%id%>"></ul>
</div>
<div class="clearleft"></div>
</div>
</script>
{% block relbar %}
{{ document.relbar|safe }}
{% endblock %}

View File

@@ -27,18 +27,25 @@ class WebSupportApp(Sphinx):
Sphinx.__init__(self, *args, **kwargs)
class WebSupport(object):
def __init__(self, srcdir='', outdir='', search=None,
"""The main API class for the web support package. All interactions
with the web support package should occur through this class.
"""
def __init__(self, srcdir='', outdir='', datadir='', search=None,
comments=None):
self.srcdir = srcdir
self.outdir = outdir or path.join(self.srcdir, '_build',
'websupport')
self.init_templating()
if search is not None:
self.init_search(search)
self._init_templating()
self.init_comments(comments)
self.outdir = outdir or datadir
if search is not None:
self._init_search(search)
self._init_comments(comments)
def init_comments(self, comments):
def _init_comments(self, comments):
if isinstance(comments, sphinxcomments.CommentBackend):
self.comments = comments
else:
@@ -51,14 +58,14 @@ class WebSupport(object):
engine = create_engine('sqlite:///%s' % db_path)
self.comments = SQLAlchemyComments(engine)
def init_templating(self):
def _init_templating(self):
import sphinx
template_path = path.join(path.dirname(sphinx.__file__),
'themes', 'basic')
loader = FileSystemLoader(template_path)
self.template_env = Environment(loader=loader)
def init_search(self, search):
def _init_search(self, search):
mod, cls = search_adapters[search]
search_class = getattr(__import__('sphinx.websupport.search.' + mod,
None, None, [cls]), cls)
@@ -67,24 +74,65 @@ class WebSupport(object):
self.results_template = \
self.template_env.get_template('searchresults.html')
def build(self, **kwargs):
doctreedir = kwargs.pop('doctreedir',
path.join(self.outdir, 'doctrees'))
def build(self):
"""Build the documentation. Places the data into the `outdir`
directory. Use it like this::
support = WebSupport(srcdir, outdir, search)
support.build()
This will read reStructured text files from `srcdir`. Then it
build the pickles and search index, placing them into `outdir`.
It will also save node data to the database.
"""
doctreedir = path.join(self.outdir, 'doctrees')
app = WebSupportApp(self.srcdir, self.srcdir,
self.outdir, doctreedir, 'websupport',
search=self.search,
comments=self.comments)
# TODO:
# Hook comments into Sphinx signals.
self.comments.pre_build()
app.build()
self.comments.post_build()
def get_document(self, docname):
"""Load and return a document from a pickle. The document will
be a dict object which can be used to render a template::
support = WebSupport(outdir=outdir)
support.get_document('index')
In most cases `docname` will be taken from the request path and
passed directly to this function. In Flask, that would be something
like this::
@app.route('/<path:docname>')
def index(docname):
q = request.args.get('q')
document = support.get_search_results(q)
render_template('doc.html', document=document)
The document dict that is returned contains the following items
to be used during template rendering.
:param docname: the name of the document to load.
"""
infilename = path.join(self.outdir, docname + '.fpickle')
f = open(infilename, 'rb')
document = pickle.load(f)
return document
def get_search_results(self, q):
"""Perform a search for the query `q`, and create a set
of search results. Then render the search results as html and
return a context dict like the one created by
:meth:`get_document`::
document = support.get_search_results(q)
:param q: the search query
"""
results, results_found, results_displayed = self.search.query(q)
ctx = {'search_performed': True,
'search_results': results,
@@ -94,14 +142,89 @@ class WebSupport(object):
document['title'] = 'Search Results'
return document
def get_comments(self, node_id, user_id):
def get_comments(self, node_id, user_id=None):
"""Get the comments associated with `node_id`. If `user_id` is
given vote information will be included with the returned comments.
The default CommentBackend returns a list of dicts. Each dict
represents a comment, and has the following items:
============ ======================================================
Key Contents
============ ======================================================
text The comment text.
username The username that was stored with the comment.
id The comment's unique identifier.
rating The comment's current rating.
age The time in seconds since the comment was added.
time A dict containing time information. It contains the
following keys: year, month, day, hour, minute, second,
iso, and delta. `iso` is the time formatted in ISO
8601 format. `delta` is a printable form of how old
the comment is (e.g. "3 hours ago").
vote If `user_id` was given, this will be an integer
representing the vote. 1 for an upvote, -1 for a
downvote, or 0 if unvoted.
node The node that the comment is attached to. If the
comment's parent is another comment rather than a
node, this will be null.
parent The id of the comment that this comment is attached
to if it is not attached to a node.
children A list of all children, in this format.
============ ======================================================
:param node_id: the id of the node to get comments for.
:param user_id: the id of the user viewing the comments.
"""
return self.comments.get_comments(node_id, user_id)
def add_comment(self, parent_id, text, displayed=True, username=None,
rating=0, time=None):
"""Add a comment to a node or another comment. `parent_id` will have
a one letter prefix, distinguishing between node parents and
comment parents, 'c' and 's' respectively. This function will
return the comment in the same format as :meth:`get_comments`.
Usage is simple::
comment = support.add_comment(parent_id, text)
If you would like to store a username with the comment, pass
in the optional `username` keyword argument::
comment = support.add_comment(parent_id, text, username=username)
:param parent_id: the prefixed id of the comment's parent.
:param text: the text of the comment.
:param displayed: for future use...
:param username: the username of the user making the comment.
:param rating: the starting rating of the comment, defaults to 0.
:param time: the time the comment was created, defaults to now.
"""
return self.comments.add_comment(parent_id, text, displayed,
username, rating, time)
def process_vote(self, comment_id, user_id, value):
"""Process a user's vote. The web support package relies
on the API user to perform authentication. The API user will
typically receive a comment_id and value from a form, and then
make sure the user is authenticated. A unique integer `user_id`
(usually the User primary key) must be passed in, which will
also be used to retrieve the user's past voting information.
An example, once again in Flask::
@app.route('/docs/process_vote', methods=['POST'])
def process_vote():
if g.user is None:
abort(401)
comment_id = request.form.get('comment_id')
value = request.form.get('value')
if value is None or comment_id is None:
abort(400)
support.process_vote(comment_id, g.user.id, value)
return "success"
:param comment_id: the comment being voted on
:param user_id: the unique integer id of the user voting
:param value: 1 for an upvote, -1 for a downvote, 0 for an unvote.
"""
value = int(value)
self.comments.process_vote(comment_id, user_id, value)

View File

@@ -1,107 +0,0 @@
# -*- coding: utf-8 -*-
"""
sphinx.websupport.api
~~~~~~~~~~~~~~~~~~~~~
All API functions.
:copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
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,
comments=None):
self.srcdir = srcdir
self.outdir = outdir or path.join(self.srcdir, '_build',
'websupport')
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
else:
# 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__),
'themes', 'basic')
loader = FileSystemLoader(template_path)
self.template_env = Environment(loader=loader)
def init_search(self, search):
mod, cls = search_adapters[search]
search_class = getattr(__import__('sphinx.websupport.search.' + mod,
None, None, [cls]), cls)
search_path = path.join(self.outdir, 'search')
self.search = search_class(search_path)
self.results_template = \
self.template_env.get_template('searchresults.html')
def build(self, **kwargs):
doctreedir = kwargs.pop('doctreedir',
path.join(self.outdir, 'doctrees'))
app = WebSupportApp(self.srcdir, self.srcdir,
self.outdir, doctreedir, 'websupport',
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')
f = open(infilename, 'rb')
document = pickle.load(f)
return document
def get_search_results(self, q):
results, results_found, results_displayed = self.search.query(q)
ctx = {'search_performed': True,
'search_results': results,
'q': q}
document = self.get_document('search')
document['body'] = self.results_template.render(ctx)
document['title'] = 'Search Results'
return document
def get_comments(self, node_id, user_id):
return self.comments.get_comments(node_id, user_id)
def add_comment(self, parent_id, text, displayed=True, username=None,
rating=0, time=None):
return self.comments.add_comment(parent_id, text, displayed,
username, rating, time)
def process_vote(self, comment_id, user_id, value):
value = int(value)
self.comments.process_vote(comment_id, user_id, value)