updated docs

This commit is contained in:
Jacob Mason 2010-08-09 14:22:31 -05:00
parent 978afa9c0d
commit ac066fb54a
12 changed files with 384 additions and 171 deletions

View File

@ -10,10 +10,45 @@ The WebSupport Class
The main API class for the web support package. All interactions The main API class for the web support package. All interactions
with the web support package should occur through this class. with the web support package should occur through this class.
:param srcdir: the directory containing the reStructuredText files The class takes the following keyword arguments:
:param outdir: the directory in which to place the built data
:param search: the search system to use srcdir
:param comments: an instance of a CommentBackend The directory containing reStructuredText source files.
builddir
The directory that build data and static files should be placed in.
This should be used when creating a :class:`WebSupport` object that
will be used to build data.
datadir:
The directory that the web support data is in. This should be used
when creating a :class:`WebSupport` object that will be used to
retrieve data.
search:
This may contain either a string (e.g. 'xapian') referencing a
built-in search adapter to use, or an instance of a subclass of
:class:`~sphinx.websupport.search.BaseSearch`.
storage:
This may contain either a string representing a database uri, or an
instance of a subclass of
:class:`~sphinx.websupport.storage.StorageBackend`. If this is not
provided a new sqlite database will be created.
moderation_callback:
A callable to be called when a new comment is added that is not
displayed. It must accept one argument: a dict representing the
comment that was added.
staticdir:
If static files are served from a location besides "/static", this
should be a string with the name of that location
(e.g. '/static_files').
docroot:
If the documentation is not served from the base path of a URL, this
should be a string specifying that path (e.g. 'docs')
Methods Methods
~~~~~~~ ~~~~~~~

View File

@ -1,6 +0,0 @@
.. _websupportfrontend:
Web Support Frontend
====================
More coming soon.

View File

@ -7,26 +7,26 @@ Building Documentation Data
~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
To make use of the web support package in your application you'll To make use of the web support package in your application you'll
need to build that data it uses. This data includes pickle files representing need to build the data it uses. This data includes pickle files representing
documents, search indices, and node data that is used to track where documents, search indices, and node data that is used to track where
comments and other things are in a document. To do this you will need comments and other things are in a document. To do this you will need
to create an instance of the :class:`~sphinx.websupport.api.WebSupport` to create an instance of the :class:`~sphinx.websupport.WebSupport`
class and call it's :meth:`~sphinx.websupport.WebSupport.build` method:: class and call it's :meth:`~sphinx.websupport.WebSupport.build` method::
from sphinx.websupport import WebSupport from sphinx.websupport import WebSupport
support = WebSupport(srcdir='/path/to/rst/sources/', support = WebSupport(srcdir='/path/to/rst/sources/',
outdir='/path/to/build/outdir', builddir='/path/to/build/outdir',
search='xapian') search='xapian')
support.build() support.build()
This will read reStructuredText sources from `srcdir` and place the This will read reStructuredText sources from `srcdir` and place the
necessary data in `outdir`. This directory contains all the data needed necessary data in `builddir`. The `builddir` will contain two
sub-directories. One named "data" that contains all the data needed
to display documents, search through documents, and add comments to to display documents, search through documents, and add comments to
documents. It will also contain a subdirectory named "static", which documents. The other directory will be called "static" and contains static
contains static files. These files will be linked to by Sphinx documents, files that should be served from "/static".
and should be served from "/static".
.. note:: .. note::
@ -37,7 +37,7 @@ and should be served from "/static".
Integrating Sphinx Documents Into Your Webapp Integrating Sphinx Documents Into Your Webapp
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now that you have the data, it's time to do something useful with it. Now that the data is built, it's time to do something useful with it.
Start off by creating a :class:`~sphinx.websupport.WebSupport` object Start off by creating a :class:`~sphinx.websupport.WebSupport` object
for your application:: for your application::
@ -59,8 +59,8 @@ This will return a dictionary containing the following items:
* **sidebar**: The sidebar of the document as HTML * **sidebar**: The sidebar of the document as HTML
* **relbar**: A div containing links to related documents * **relbar**: A div containing links to related documents
* **title**: The title of the document * **title**: The title of the document
* **DOCUMENTATION_OPTIONS**: Javascript containing documentation options * **css**: Links to css files used by Sphinx
* **COMMENT_OPTIONS**: Javascript containing comment options * **js**: Javascript containing comment options
This dict can then be used as context for templates. The goal is to be 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 easy to integrate with your existing templating system. An example using
@ -74,13 +74,15 @@ easy to integrate with your existing templating system. An example using
{{ document.title }} {{ document.title }}
{%- endblock %} {%- endblock %}
{%- block js %} {% block css %}
<script type="text/javascript">
{{ document.DOCUMENTATION_OPTIONS|safe }}
{{ document.COMMENT_OPTIONS|safe }}
</script>
{{ super() }} {{ super() }}
<script type="text/javascript" src="/static/websupport.js"></script> {{ document.css|safe }}
<link rel="stylesheet" href="/static/websupport-custom.css" type="text/css">
{% endblock %}
{%- block js %}
{{ super() }}
{{ document.js|safe }}
{%- endblock %} {%- endblock %}
{%- block relbar %} {%- block relbar %}
@ -99,12 +101,12 @@ Authentication
-------------- --------------
To use certain features such as voting it must be possible to authenticate To use certain features such as voting it must be possible to authenticate
users. The details of the authentication are left to the your application. users. The details of the authentication are left to your application.
Once a user has been authenticated you can pass the user's details to certain Once a user has been authenticated you can pass the user's details to certain
:class:`~sphinx.websupport.WebSupport` methods using the *username* and :class:`~sphinx.websupport.WebSupport` methods using the *username* and
*moderator* keyword arguments. The web support package will store the *moderator* keyword arguments. The web support package will store the
username with comments and votes. The only caveat is that if you allow users username with comments and votes. The only caveat is that if you allow users
to change their username, you must update the websupport package's data:: to change their username you must update the websupport package's data::
support.update_username(old_username, new_username) support.update_username(old_username, new_username)
@ -113,18 +115,22 @@ should be a boolean representing whether the user has moderation
privilieges. The default value for *moderator* is *False*. privilieges. The default value for *moderator* is *False*.
An example `Flask <http://flask.pocoo.org/>`_ function that checks whether An example `Flask <http://flask.pocoo.org/>`_ function that checks whether
a user is logged in, and the retrieves a document is:: a user is logged in and then retrieves a document is::
from sphinx.websupport.errors import *
@app.route('/<path:docname>') @app.route('/<path:docname>')
def doc(docname): def doc(docname):
if g.user: username = g.user.name if g.user else ''
document = support.get_document(docname, g.user.name, moderator = g.user.moderator if g.user else False
g.user.moderator) try:
else: document = support.get_document(docname, username, moderator)
document = support.get_document(docname) except DocumentNotFoundError:
abort(404)
return render_template('doc.html', document=document) return render_template('doc.html', document=document)
The first thing to notice is that the *docname* is just the request path. The first thing to notice is that the *docname* is just the request path.
This makes accessing the correct document easy from a single view.
If the user is authenticated then the username and moderation status are If the user is authenticated then the username and moderation status are
passed along with the docname to passed along with the docname to
:meth:`~sphinx.websupport.WebSupport.get_document`. The web support package :meth:`~sphinx.websupport.WebSupport.get_document`. The web support package
@ -134,8 +140,12 @@ will then add this data to the COMMENT_OPTIONS that are used in the template.
This only works works if your documentation is served from your This only works works if your documentation is served from your
document root. If it is served from another directory, you will document root. If it is served from another directory, you will
need to prefix the url route with that directory:: need to prefix the url route with that directory, and give the `docroot`
keyword argument when creating the web support object::
support = WebSupport(...
docroot='docs')
@app.route('/docs/<path:docname>') @app.route('/docs/<path:docname>')
Performing Searches Performing Searches
@ -160,8 +170,8 @@ did to render our documents. That's because
dict in the same format that dict in the same format that
:meth:`~sphinx.websupport.WebSupport.get_document` does. :meth:`~sphinx.websupport.WebSupport.get_document` does.
Comments Comments & Proposals
~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
Now that this is done it's time to define the functions that handle Now that this is done it's time to define the functions that handle
the AJAX calls from the script. You will need three functions. The first the AJAX calls from the script. You will need three functions. The first
@ -171,20 +181,29 @@ function is used to add a new comment, and will call the web support method
@app.route('/docs/add_comment', methods=['POST']) @app.route('/docs/add_comment', methods=['POST'])
def add_comment(): def add_comment():
parent_id = request.form.get('parent', '') parent_id = request.form.get('parent', '')
node_id = request.form.get('node', '')
text = request.form.get('text', '') text = request.form.get('text', '')
proposal = request.form.get('proposal', '')
username = g.user.name if g.user is not None else 'Anonymous' username = g.user.name if g.user is not None else 'Anonymous'
comment = support.add_comment(parent_id, text, username=username) comment = support.add_comment(text, node_id='node_id',
parent_id='parent_id',
username=username, proposal=proposal)
return jsonify(comment=comment) return jsonify(comment=comment)
Then next function handles the retrieval of comments for a specific node, You'll notice that both a `parent_id` and `node_id` are sent with the
and is aptly named :meth:`~sphinx.websupport.WebSupport.get_data`:: request. If the comment is being attached directly to a node, `parent_id`
will be empty. If the comment is a child of another comment, then `node_id`
will be empty. Then next function handles the retrieval of comments for a
specific node, and is aptly named
:meth:`~sphinx.websupport.WebSupport.get_data`::
@app.route('/docs/get_comments') @app.route('/docs/get_comments')
def get_comments(): def get_comments():
user_id = g.user.id if g.user else None username = g.user.name if g.user else None
parent_id = request.args.get('parent', '') moderator = g.user.moderator if g.user else False
comments = support.get_data(parent_id, user_id) node_id = request.args.get('node', '')
return jsonify(comments=comments) data = support.get_data(parent_id, user_id)
return jsonify(**data)
The final function that is needed will call The final function that is needed will call
:meth:`~sphinx.websupport.WebSupport.process_vote`, and will handle user :meth:`~sphinx.websupport.WebSupport.process_vote`, and will handle user
@ -201,12 +220,49 @@ votes on comments::
support.process_vote(comment_id, g.user.id, value) support.process_vote(comment_id, g.user.id, value)
return "success" return "success"
.. note:: Comment Moderation
~~~~~~~~~~~~~~~~~~
Authentication is left up to your existing web application. If you do By default all comments added through
not have an existing authentication system there are many readily :meth:`~sphinx.websupport.WebSupport.add_comment` are automatically
available for different frameworks. The web support system stores only displayed. If you wish to have some form of moderation, you can pass
the user's unique integer `user_id` and uses this both for storing votes the `displayed` keyword argument::
and retrieving vote information. It is up to you to ensure that the
user_id passed in is unique, and that the user is authenticated. The comment = support.add_comment(text, node_id='node_id',
default backend will only allow one vote per comment per `user_id`. parent_id='parent_id',
username=username, proposal=proposal,
displayed=False)
You can then create two new views to handle the moderation of comments. The
first will be called when a moderator decides a comment should be accepted
and displayed::
@app.route('/docs/accept_comment', methods=['POST'])
def accept_comment():
moderator = g.user.moderator if g.user else False
comment_id = request.form.get('id')
support.accept_comment(comment_id, moderator=moderator)
return 'OK'
The next is very similar, but used when rejecting a comment::
@app.route('/docs/reject_comment', methods=['POST'])
def reject_comment():
moderator = g.user.moderator if g.user else False
comment_id = request.form.get('id')
support.reject_comment(comment_id, moderator=moderator)
return 'OK'
To perform a custom action (such as emailing a moderator) when a new comment
is added but not displayed, you can pass callable to the
:class:`~sphinx.websupport.WebSupport` class when instantiating your support
object::
def moderation_callback(comment):
Do something...
support = WebSupport(...
moderation_callback=moderation_callback)
The moderation callback must take one argument, which will be the same
comment dict that is returned by add_comment.

View File

@ -11,7 +11,7 @@ and pass that as the `search` keyword argument when you create the
:class:`~sphinx.websupport.WebSupport` object:: :class:`~sphinx.websupport.WebSupport` object::
support = Websupport(srcdir=srcdir, support = Websupport(srcdir=srcdir,
outdir=outdir, builddir=builddir,
search=MySearch()) search=MySearch())
For more information about creating a custom search adapter, please see For more information about creating a custom search adapter, please see

View File

@ -5,6 +5,22 @@
Storage Backends Storage Backends
================ ================
To create a custom storage backend you will need to subclass the
:class:`~StorageBackend` class. Then create an instance of the new class
and pass that as the `storage` keyword argument when you create the
:class:`~sphinx.websupport.WebSupport` object::
support = Websupport(srcdir=srcdir,
builddir=builddir,
storage=MyStorage())
For more information about creating a custom storage backend, please see
the documentation of the :class:`StorageBackend` class below.
.. class:: StorageBackend
Defines an interface for storage backends.
StorageBackend Methods StorageBackend Methods
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
@ -16,4 +32,14 @@ StorageBackend Methods
.. automethod:: sphinx.websupport.storage.StorageBackend.add_comment .. automethod:: sphinx.websupport.storage.StorageBackend.add_comment
.. automethod:: sphinx.websupport.storage.StorageBackend.delete_comment
.. automethod:: sphinx.websupport.storage.StorageBackend.get_data .. automethod:: sphinx.websupport.storage.StorageBackend.get_data
.. automethod:: sphinx.websupport.storage.StorageBackend.process_vote
.. automethod:: sphinx.websupport.storage.StorageBackend.update_username
.. automethod:: sphinx.websupport.storage.StorageBackend.accept_comment
.. automethod:: sphinx.websupport.storage.StorageBackend.reject_comment

View File

@ -11,6 +11,5 @@ into your web application. To learn more read the
web/quickstart web/quickstart
web/api web/api
web/frontend
web/searchadapters web/searchadapters
web/storagebackends web/storagebackends

View File

@ -52,6 +52,8 @@ class WebSupport(object):
self._init_search(search) self._init_search(search)
self._init_storage(storage) self._init_storage(storage)
self._make_base_comment_options()
def _init_storage(self, storage): def _init_storage(self, storage):
if isinstance(storage, StorageBackend): if isinstance(storage, StorageBackend):
self.storage = storage self.storage = storage
@ -90,11 +92,11 @@ class WebSupport(object):
"""Build the documentation. Places the data into the `outdir` """Build the documentation. Places the data into the `outdir`
directory. Use it like this:: directory. Use it like this::
support = WebSupport(srcdir, outdir, search='xapian') support = WebSupport(srcdir, builddir, search='xapian')
support.build() support.build()
This will read reStructured text files from `srcdir`. Then it This will read reStructured text files from `srcdir`. Then it will
build the pickles and search index, placing them into `outdir`. build the pickles and search index, placing them into `builddir`.
It will also save node data to the database. It will also save node data to the database.
""" """
if not self.srcdir: if not self.srcdir:
@ -116,7 +118,7 @@ class WebSupport(object):
be a dict object which can be used to render a template:: be a dict object which can be used to render a template::
support = WebSupport(datadir=datadir) support = WebSupport(datadir=datadir)
support.get_document('index') support.get_document('index', username, moderator)
In most cases `docname` will be taken from the request path and In most cases `docname` will be taken from the request path and
passed directly to this function. In Flask, that would be something passed directly to this function. In Flask, that would be something
@ -124,13 +126,28 @@ class WebSupport(object):
@app.route('/<path:docname>') @app.route('/<path:docname>')
def index(docname): def index(docname):
q = request.args.get('q') username = g.user.name if g.user else ''
document = support.get_search_results(q) moderator = g.user.moderator if g.user else False
try:
document = support.get_document(docname, username,
moderator)
except DocumentNotFoundError:
abort(404)
render_template('doc.html', document=document) render_template('doc.html', document=document)
The document dict that is returned contains the following items The document dict that is returned contains the following items
to be used during template rendering. to be used during template rendering.
* **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
* **css**: Links to css files used by Sphinx
* **js**: Javascript containing comment options
This raises :class:`~sphinx.websupport.errors.DocumentNotFoundError`
if a document matching `docname` is not found.
:param docname: the name of the document to load. :param docname: the name of the document to load.
""" """
infilename = path.join(self.datadir, 'pickles', docname + '.fpickle') infilename = path.join(self.datadir, 'pickles', docname + '.fpickle')
@ -146,39 +163,6 @@ class WebSupport(object):
document['js'] = comment_opts + '\n' + document['js'] document['js'] = comment_opts + '\n' + document['js']
return document return document
def _make_comment_options(self, username, moderator):
parts = ['<script type="text/javascript">',
'var COMMENT_OPTIONS = {']
if self.docroot is not '':
parts.append('addCommentURL: "/%s/%s",' % (self.docroot,
'add_comment'))
parts.append('getCommentsURL: "/%s/%s",' % (self.docroot,
'get_comments'))
parts.append('processVoteURL: "/%s/%s",' % (self.docroot,
'process_vote'))
parts.append('acceptCommentURL: "/%s/%s",' % (self.docroot,
'accept_comment'))
parts.append('rejectCommentURL: "/%s/%s",' % (self.docroot,
'reject_comment'))
parts.append('deleteCommentURL: "/%s/%s",' % (self.docroot,
'delete_comment'))
if self.staticdir != 'static':
p = lambda file: '%s/_static/%s' % (self.staticdir, file)
parts.append('commentImage: "/%s",' % p('comment.png') )
parts.append('upArrow: "/%s",' % p('up.png'))
parts.append('downArrow: "/%s",' % p('down.png'))
parts.append('upArrowPressed: "/%s",' % p('up-pressed.png'))
parts.append('downArrowPressed: "/%s",' % p('down-pressed.png'))
if username is not '':
parts.append('voting: true,')
parts.append('username: "%s",' % username)
parts.append('moderator: %s' % str(moderator).lower())
parts.append('};')
parts.append('</script>')
return '\n'.join(parts)
def get_search_results(self, q): def get_search_results(self, q):
"""Perform a search for the query `q`, and create a set """Perform a search for the query `q`, and create a set
of search results. Then render the search results as html and of search results. Then render the search results as html and
@ -200,37 +184,42 @@ class WebSupport(object):
def get_data(self, node_id, username=None, moderator=False): def get_data(self, node_id, username=None, moderator=False):
"""Get the comments and source associated with `node_id`. If """Get the comments and source associated with `node_id`. If
`user_id` is given vote information will be included with the `username` is given vote information will be included with the
returned comments. The default CommentBackend returns dict with returned comments. The default CommentBackend returns a dict with
two keys, *source*, and *comments*. *comments* is a list of two keys, *source*, and *comments*. *source* is raw source of the
dicts that represent a comment, each having the following items: node and is used as the starting point for proposals a user can
add. *comments* is a list of dicts that represent a comment, each
having the following items:
============ ====================================================== ============= ======================================================
Key Contents Key Contents
============ ====================================================== ============= ======================================================
text The comment text. text The comment text.
username The username that was stored with the comment. username The username that was stored with the comment.
id The comment's unique identifier. id The comment's unique identifier.
rating The comment's current rating. rating The comment's current rating.
age The time in seconds since the comment was added. age The time in seconds since the comment was added.
time A dict containing time information. It contains the time A dict containing time information. It contains the
following keys: year, month, day, hour, minute, second, following keys: year, month, day, hour, minute, second,
iso, and delta. `iso` is the time formatted in ISO iso, and delta. `iso` is the time formatted in ISO
8601 format. `delta` is a printable form of how old 8601 format. `delta` is a printable form of how old
the comment is (e.g. "3 hours ago"). the comment is (e.g. "3 hours ago").
vote If `user_id` was given, this will be an integer vote If `user_id` was given, this will be an integer
representing the vote. 1 for an upvote, -1 for a representing the vote. 1 for an upvote, -1 for a
downvote, or 0 if unvoted. downvote, or 0 if unvoted.
node The node that the comment is attached to. If the node The id of the node that the comment is attached to.
comment's parent is another comment rather than a If the comment's parent is another comment rather than
node, this will be null. a node, this will be null.
parent The id of the comment that this comment is attached parent The id of the comment that this comment is attached
to if it is not attached to a node. to if it is not attached to a node.
children A list of all children, in this format. children A list of all children, in this format.
============ ====================================================== proposal_diff An HTML representation of the differences between the
the current source and the user's proposed source.
============= ======================================================
:param node_id: the id of the node to get comments for. :param node_id: the id of the node to get comments for.
:param user_id: the id of the user viewing the comments. :param username: the username of the user viewing the comments.
:param moderator: whether the user is a moderator.
""" """
return self.storage.get_data(node_id, username, moderator) return self.storage.get_data(node_id, username, moderator)
@ -243,6 +232,10 @@ class WebSupport(object):
`moderator` is False, the comment will only be deleted if the `moderator` is False, the comment will only be deleted if the
`username` matches the `username` on the comment. `username` matches the `username` on the comment.
This raises :class:`~sphinx.websupport.errors.UserNotAuthorizedError`
if moderator is False and `username` doesn't match username on the
comment.
:param comment_id: the id of the comment to delete. :param comment_id: the id of the comment to delete.
:param username: the username requesting the deletion. :param username: the username requesting the deletion.
:param moderator: whether the requestor is a moderator. :param moderator: whether the requestor is a moderator.
@ -250,19 +243,19 @@ class WebSupport(object):
self.storage.delete_comment(comment_id, username, moderator) self.storage.delete_comment(comment_id, username, moderator)
def add_comment(self, text, node_id='', parent_id='', displayed=True, def add_comment(self, text, node_id='', parent_id='', displayed=True,
username=None, rating=0, time=None, proposal=None, username=None, time=None, proposal=None,
moderator=False): moderator=False):
"""Add a comment to a node or another comment. Returns the comment """Add a comment to a node or another comment. Returns the comment
in the same format as :meth:`get_comments`. If the comment is being in the same format as :meth:`get_comments`. If the comment is being
attached to a node, pass in the node's id (as a string) with the attached to a node, pass in the node's id (as a string) with the
node keyword argument:: node keyword argument::
comment = support.add_comment(text, node=node_id) comment = support.add_comment(text, node_id=node_id)
If the comment is the child of another comment, provide the parent's If the comment is the child of another comment, provide the parent's
id (as a string) with the parent keyword argument:: id (as a string) with the parent keyword argument::
comment = support.add_comment(text, parent=parent_id) comment = support.add_comment(text, parent_id=parent_id)
If you would like to store a username with the comment, pass If you would like to store a username with the comment, pass
in the optional `username` keyword argument:: in the optional `username` keyword argument::
@ -274,10 +267,9 @@ class WebSupport(object):
:param text: the text of the comment. :param text: the text of the comment.
:param displayed: for moderation purposes :param displayed: for moderation purposes
:param username: the username of the user making the comment. :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. :param time: the time the comment was created, defaults to now.
""" """
comment = self.storage.add_comment(text, displayed, username, rating, comment = self.storage.add_comment(text, displayed, username,
time, proposal, node_id, time, proposal, node_id,
parent_id, moderator) parent_id, moderator)
if not displayed and self.moderation_callback: if not displayed and self.moderation_callback:
@ -288,10 +280,9 @@ class WebSupport(object):
"""Process a user's vote. The web support package relies """Process a user's vote. The web support package relies
on the API user to perform authentication. The API user will on the API user to perform authentication. The API user will
typically receive a comment_id and value from a form, and then typically receive a comment_id and value from a form, and then
make sure the user is authenticated. A unique integer `user_id` make sure the user is authenticated. A unique username must be
(usually the User primary key) must be passed in, which will passed in, which will also be used to retrieve the user's past
also be used to retrieve the user's past voting information. voting data. An example, once again in Flask::
An example, once again in Flask::
@app.route('/docs/process_vote', methods=['POST']) @app.route('/docs/process_vote', methods=['POST'])
def process_vote(): def process_vote():
@ -301,11 +292,11 @@ class WebSupport(object):
value = request.form.get('value') value = request.form.get('value')
if value is None or comment_id is None: if value is None or comment_id is None:
abort(400) abort(400)
support.process_vote(comment_id, g.user.id, value) support.process_vote(comment_id, g.user.name, value)
return "success" return "success"
:param comment_id: the comment being voted on :param comment_id: the comment being voted on
:param user_id: the unique integer id of the user voting :param username: the unique username of the user voting
:param value: 1 for an upvote, -1 for a downvote, 0 for an unvote. :param value: 1 for an upvote, -1 for a downvote, 0 for an unvote.
""" """
value = int(value) value = int(value)
@ -329,7 +320,11 @@ class WebSupport(object):
def accept_comment(self, comment_id, moderator=False): def accept_comment(self, comment_id, moderator=False):
"""Accept a comment that is pending moderation. """Accept a comment that is pending moderation.
This raises :class:`~sphinx.websupport.errors.UserNotAuthorizedError`
if moderator is False.
:param comment_id: The id of the comment that was accepted. :param comment_id: The id of the comment that was accepted.
:param moderator: Whether the user making the request is a moderator.
""" """
if not moderator: if not moderator:
raise UserNotAuthorizedError() raise UserNotAuthorizedError()
@ -338,8 +333,60 @@ class WebSupport(object):
def reject_comment(self, comment_id, moderator=False): def reject_comment(self, comment_id, moderator=False):
"""Reject a comment that is pending moderation. """Reject a comment that is pending moderation.
This raises :class:`~sphinx.websupport.errors.UserNotAuthorizedError`
if moderator is False.
:param comment_id: The id of the comment that was accepted. :param comment_id: The id of the comment that was accepted.
:param moderator: Whether the user making the request is a moderator.
""" """
if not moderator: if not moderator:
raise UserNotAuthorizedError() raise UserNotAuthorizedError()
self.storage.reject_comment(comment_id) self.storage.reject_comment(comment_id)
def _make_base_comment_options(self):
"""Helper method to create the part of the COMMENT_OPTIONS javascript
that remains the same throughout the lifetime of the
:class:`~sphinx.websupport.WebSupport` object.
"""
parts = ['<script type="text/javascript">',
'var COMMENT_OPTIONS = {']
if self.docroot is not '':
parts.append('addCommentURL: "/%s/%s",' % (self.docroot,
'add_comment'))
parts.append('getCommentsURL: "/%s/%s",' % (self.docroot,
'get_comments'))
parts.append('processVoteURL: "/%s/%s",' % (self.docroot,
'process_vote'))
parts.append('acceptCommentURL: "/%s/%s",' % (self.docroot,
'accept_comment'))
parts.append('rejectCommentURL: "/%s/%s",' % (self.docroot,
'reject_comment'))
parts.append('deleteCommentURL: "/%s/%s",' % (self.docroot,
'delete_comment'))
if self.staticdir != 'static':
p = lambda file: '%s/_static/%s' % (self.staticdir, file)
parts.append('commentImage: "/%s",' % p('comment.png') )
parts.append('upArrow: "/%s",' % p('up.png'))
parts.append('downArrow: "/%s",' % p('down.png'))
parts.append('upArrowPressed: "/%s",' % p('up-pressed.png'))
parts.append('downArrowPressed: "/%s",' % p('down-pressed.png'))
self.base_comment_opts = '\n'.join(parts)
def _make_comment_options(self, username, moderator):
"""Helper method to create the parts of the COMMENT_OPTIONS
javascript that are unique to each request.
:param username: The username of the user making the request.
:param moderator: Whether the user making the request is a moderator.
"""
parts = [self.base_comment_opts]
if username is not '':
parts.append('voting: true,')
parts.append('username: "%s",' % username)
parts.append('moderator: %s' % str(moderator).lower())
parts.append('};')
parts.append('</script>')
return '\n'.join(parts)

View File

@ -20,7 +20,7 @@ class BaseSearch(object):
is a list of pagenames that will be reindexed. You may want to remove is a list of pagenames that will be reindexed. You may want to remove
these from the search index before indexing begins. these from the search index before indexing begins.
`param changed` is a list of pagenames that will be re-indexed :param changed: a list of pagenames that will be re-indexed
""" """
pass pass
@ -37,11 +37,9 @@ class BaseSearch(object):
won't want to override this unless you need access to the `doctree`. won't want to override this unless you need access to the `doctree`.
Override :meth:`add_document` instead. Override :meth:`add_document` instead.
`pagename` is the name of the page to be indexed :param pagename: the name of the page to be indexed
:param title: the title of the page to be indexed
`title` is the title of the page to be indexed :param doctree: is the docutils doctree representation of the page
`doctree` is the docutils doctree representation of the page
""" """
self.add_document(pagename, title, doctree.astext()) self.add_document(pagename, title, doctree.astext())
@ -50,18 +48,16 @@ class BaseSearch(object):
This method should should do everything necessary to add a single This method should should do everything necessary to add a single
document to the search index. document to the search index.
`pagename` is name of the page being indexed. `pagename` is name of the page being indexed. It is the combination
It is the combination of the source files relative path and filename, of the source files relative path and filename,
minus the extension. For example, if the source file is minus the extension. For example, if the source file is
"ext/builders.rst", the `pagename` would be "ext/builders". This "ext/builders.rst", the `pagename` would be "ext/builders". This
will need to be returned with search results when processing a will need to be returned with search results when processing a
query. query.
`title` is the page's title, and will need to be returned with :param pagename: the name of the page being indexed
search results. :param title: the page's title
:param text: the full text of the page
`text` is the full text of the page. You probably want to store this
somehow to use while creating the context for search results.
""" """
raise NotImplementedError() raise NotImplementedError()
@ -73,7 +69,7 @@ class BaseSearch(object):
don't want to use the included :meth:`extract_context` method. don't want to use the included :meth:`extract_context` method.
Override :meth:`handle_query` instead. Override :meth:`handle_query` instead.
`q` is the search query string. :param q: the search query string.
""" """
self.context_re = re.compile('|'.join(q.split()), re.I) self.context_re = re.compile('|'.join(q.split()), re.I)
return self.handle_query(q) return self.handle_query(q)
@ -91,6 +87,8 @@ class BaseSearch(object):
The :meth:`extract_context` method is provided as a simple way The :meth:`extract_context` method is provided as a simple way
to create the `context`. to create the `context`.
:param q: the search query
""" """
raise NotImplementedError() raise NotImplementedError()
@ -98,9 +96,8 @@ class BaseSearch(object):
"""Extract the context for the search query from the documents """Extract the context for the search query from the documents
full `text`. full `text`.
`text` is the full text of the document to create the context for. :param text: the full text of the document to create the context for
:param length: the length of the context snippet to return.
`length` is the length of the context snippet to return.
""" """
res = self.context_re.search(text) res = self.context_re.search(text)
if res is None: if res is None:

View File

@ -16,16 +16,14 @@ class StorageBackend(object):
""" """
pass pass
def add_node(self, document, line, source, treeloc): def add_node(self, document, line, source):
"""Add a node to the StorageBackend. """Add a node to the StorageBackend.
`document` is the name of the document the node belongs to. :param document: the name of the document the node belongs to.
`line` is the line in the source where the node begins. :param line: the line in the source where the node begins.
`source` is the source files name. :param source: the source files name.
`treeloc` is for future use.
""" """
raise NotImplementedError() raise NotImplementedError()
@ -35,14 +33,77 @@ class StorageBackend(object):
""" """
pass pass
def add_comment(self, text, displayed, username, rating, time, def add_comment(self, text, displayed, username, time,
proposal, node, parent): proposal, node_id, parent_id, moderator):
"""Called when a comment is being added.""" """Called when a comment is being added.
:param text: the text of the comment
:param displayed: whether the comment should be displayed
:param username: the name of the user adding the comment
:param time: a date object with the time the comment was added
:param proposal: the text of the proposal the user made
:param node_id: the id of the node that the comment is being added to
:param parent_id: the id of the comment's parent comment.
:param moderator: whether the user adding the comment is a moderator
"""
raise NotImplementedError() raise NotImplementedError()
def get_data(self, parent_id, user_id, moderator): def delete_comment(self, comment_id, username, moderator):
"""Called to retrieve all comments for a node.""" """Delete a comment.
Raises :class:`~sphinx.websupport.errors.UserNotAuthorizedError`
if moderator is False and `username` doesn't match the username
on the comment.
:param comment_id: The id of the comment being deleted.
:param username: The username of the user requesting the deletion.
:param moderator: Whether the user is a moderator.
"""
raise NotImplementedError() raise NotImplementedError()
def process_vote(self, comment_id, user_id, value): def get_data(self, node_id, username, moderator):
"""Called to retrieve all data for a node. This should return a
dict with two keys, *source* and *comments* as described by
:class:`~sphinx.websupport.WebSupport`'s
:meth:`~sphinx.websupport.WebSupport.get_data` method.
:param node_id: The id of the node to get data for.
:param username: The name of the user requesting the data.
:param moderator: Whether the requestor is a moderator.
"""
raise NotImplementedError()
def process_vote(self, comment_id, username, value):
"""Process a vote that is being cast. `value` will be either -1, 0,
or 1.
:param comment_id: The id of the comment being voted on.
:param username: The username of the user casting the vote.
:param value: The value of the vote being cast.
"""
raise NotImplementedError()
def update_username(self, old_username, new_username):
"""If a user is allowed to change their username this method should
be called so that there is not stagnate data in the storage system.
:param old_username: The username being changed.
:param new_username: What the username is being changed to.
"""
raise NotImplementedError()
def accept_comment(self, comment_id):
"""Called when a moderator accepts a comment. After the method is
called the comment should be displayed to all users.
:param comment_id: The id of the comment being accepted.
"""
raise NotImplementedError()
def reject_comment(self, comment_id):
"""Called when a moderator rejects a comment. The comment should
then be deleted.
:param comment_id: The id of the comment being accepted.
"""
raise NotImplementedError() raise NotImplementedError()

View File

@ -81,11 +81,10 @@ class Node(Base):
return comments return comments
def __init__(self, document, line, source, treeloc): def __init__(self, document, line, source):
self.document = document self.document = document
self.line = line self.line = line
self.source = source self.source = source
self.treeloc = treeloc
class Comment(Base): class Comment(Base):
__tablename__ = db_prefix + 'comments' __tablename__ = db_prefix + 'comments'

View File

@ -27,8 +27,8 @@ class SQLAlchemyStorage(StorageBackend):
def pre_build(self): def pre_build(self):
self.build_session = Session() self.build_session = Session()
def add_node(self, document, line, source, treeloc): def add_node(self, document, line, source):
node = Node(document, line, source, treeloc) node = Node(document, line, source)
self.build_session.add(node) self.build_session.add(node)
self.build_session.flush() self.build_session.flush()
return node return node
@ -37,7 +37,7 @@ class SQLAlchemyStorage(StorageBackend):
self.build_session.commit() self.build_session.commit()
self.build_session.close() self.build_session.close()
def add_comment(self, text, displayed, username, rating, time, def add_comment(self, text, displayed, username, time,
proposal, node_id, parent_id, moderator): proposal, node_id, parent_id, moderator):
session = Session() session = Session()
@ -55,7 +55,7 @@ class SQLAlchemyStorage(StorageBackend):
else: else:
proposal_diff = None proposal_diff = None
comment = Comment(text, displayed, username, rating, comment = Comment(text, displayed, username, 0,
time or datetime.now(), proposal, proposal_diff) time or datetime.now(), proposal, proposal_diff)
session.add(comment) session.add(comment)
session.flush() session.flush()

View File

@ -57,6 +57,5 @@ class WebSupportTranslator(HTMLTranslator):
storage = self.builder.app.storage storage = self.builder.app.storage
db_node_id = storage.add_node(document=self.builder.cur_docname, db_node_id = storage.add_node(document=self.builder.cur_docname,
line=node.line, line=node.line,
source=node.rawsource or node.astext(), source=node.rawsource or node.astext())
treeloc='???')
return db_node_id return db_node_id