1.. _websupportquickstart:
2
3Web Support Quick Start
4=======================
5
6Building Documentation Data
7----------------------------
8
9To make use of the web support package in your application you'll need to build
10the data it uses.  This data includes pickle files representing documents,
11search indices, and node data that is used to track where comments and other
12things are in a document.  To do this you will need to create an instance of the
13:class:`~.WebSupport` class and call its :meth:`~.WebSupport.build` method::
14
15   from sphinxcontrib.websupport import WebSupport
16
17   support = WebSupport(srcdir='/path/to/rst/sources/',
18                        builddir='/path/to/build/outdir',
19                        search='xapian')
20
21   support.build()
22
23This will read reStructuredText sources from ``srcdir`` and place the necessary
24data in ``builddir``.  The ``builddir`` will contain two sub-directories: one
25named "data" that contains all the data needed to display documents, search
26through documents, and add comments to documents.  The other directory will be
27called "static" and contains static files that should be served from "/static".
28
29.. note::
30
31   If you wish to serve static files from a path other than "/static", you can
32   do so by providing the *staticdir* keyword argument when creating the
33   :class:`~.WebSupport` object.
34
35
36Integrating Sphinx Documents Into Your Webapp
37----------------------------------------------
38
39Now that the data is built, it's time to do something useful with it.  Start off
40by creating a :class:`~.WebSupport` object for your application::
41
42   from sphinxcontrib.websupport import WebSupport
43
44   support = WebSupport(datadir='/path/to/the/data',
45                        search='xapian')
46
47You'll only need one of these for each set of documentation you will be working
48with.  You can then call its :meth:`~.WebSupport.get_document` method to access
49individual documents::
50
51   contents = support.get_document('contents')
52
53This will return a dictionary containing the following items:
54
55* **body**: The main body of the document as HTML
56* **sidebar**: The sidebar of the document as HTML
57* **relbar**: A div containing links to related documents
58* **title**: The title of the document
59* **css**: Links to CSS files used by Sphinx
60* **script**: JavaScript containing comment options
61
62This dict can then be used as context for templates.  The goal is to be easy to
63integrate with your existing templating system.  An example using `Jinja2
64<http://jinja.pocoo.org/>`_ is:
65
66.. code-block:: html+jinja
67
68   {%- extends "layout.html" %}
69
70   {%- block title %}
71       {{ document.title }}
72   {%- endblock %}
73
74   {% block css %}
75       {{ super() }}
76       {{ document.css|safe }}
77       <link rel="stylesheet" href="/static/websupport-custom.css" type="text/css">
78   {% endblock %}
79
80   {%- block script %}
81       {{ super() }}
82       {{ document.script|safe }}
83   {%- endblock %}
84
85   {%- block relbar %}
86       {{ document.relbar|safe }}
87   {%- endblock %}
88
89   {%- block body %}
90       {{ document.body|safe }}
91   {%- endblock %}
92
93   {%- block sidebar %}
94       {{ document.sidebar|safe }}
95   {%- endblock %}
96
97
98Authentication
99~~~~~~~~~~~~~~
100
101To use certain features such as voting, it must be possible to authenticate
102users.  The details of the authentication are left to your application.  Once a
103user has been authenticated you can pass the user's details to certain
104:class:`~.WebSupport` methods using the *username* and *moderator* keyword
105arguments.  The web support package will store the username with comments and
106votes.  The only caveat is that if you allow users to change their username you
107must update the websupport package's data::
108
109   support.update_username(old_username, new_username)
110
111*username* should be a unique string which identifies a user, and *moderator*
112should be a boolean representing whether the user has moderation privileges.
113The default value for *moderator* is ``False``.
114
115An example `Flask <http://flask.pocoo.org/>`_ function that checks whether a
116user is logged in and then retrieves a document is::
117
118   from sphinxcontrib.websupport.errors import *
119
120   @app.route('/<path:docname>')
121   def doc(docname):
122       username = g.user.name if g.user else ''
123       moderator = g.user.moderator if g.user else False
124       try:
125           document = support.get_document(docname, username, moderator)
126       except DocumentNotFoundError:
127           abort(404)
128       return render_template('doc.html', document=document)
129
130The first thing to notice is that the *docname* is just the request path.  This
131makes accessing the correct document easy from a single view.  If the user is
132authenticated, then the username and moderation status are passed along with the
133docname to :meth:`~.WebSupport.get_document`.  The web support package will then
134add this data to the ``COMMENT_OPTIONS`` that are used in the template.
135
136.. note::
137
138   This only works if your documentation is served from your
139   document root. If it is served from another directory, you will
140   need to prefix the url route with that directory, and give the `docroot`
141   keyword argument when creating the web support object::
142
143      support = WebSupport(..., docroot='docs')
144
145      @app.route('/docs/<path:docname>')
146
147
148Performing Searches
149-------------------
150
151To use the search form built-in to the Sphinx sidebar, create a function to
152handle requests to the URL 'search' relative to the documentation root.  The
153user's search query will be in the GET parameters, with the key `q`.  Then use
154the :meth:`~sphinxcontrib.websupport.WebSupport.get_search_results` method to
155retrieve search results. In `Flask <http://flask.pocoo.org/>`_ that would be
156like this::
157
158   @app.route('/search')
159   def search():
160       q = request.args.get('q')
161       document = support.get_search_results(q)
162       return render_template('doc.html', document=document)
163
164Note that we used the same template to render our search results as we did to
165render our documents.  That's because :meth:`~.WebSupport.get_search_results`
166returns a context dict in the same format that :meth:`~.WebSupport.get_document`
167does.
168
169
170Comments & Proposals
171--------------------
172
173Now that this is done it's time to define the functions that handle the AJAX
174calls from the script.  You will need three functions.  The first function is
175used to add a new comment, and will call the web support method
176:meth:`~.WebSupport.add_comment`::
177
178   @app.route('/docs/add_comment', methods=['POST'])
179   def add_comment():
180       parent_id = request.form.get('parent', '')
181       node_id = request.form.get('node', '')
182       text = request.form.get('text', '')
183       proposal = request.form.get('proposal', '')
184       username = g.user.name if g.user is not None else 'Anonymous'
185       comment = support.add_comment(text, node_id='node_id',
186                                     parent_id='parent_id',
187                                     username=username, proposal=proposal)
188       return jsonify(comment=comment)
189
190You'll notice that both a ``parent_id`` and ``node_id`` are sent with the
191request. If the comment is being attached directly to a node, ``parent_id``
192will be empty. If the comment is a child of another comment, then ``node_id``
193will be empty. Then next function handles the retrieval of comments for a
194specific node, and is aptly named
195:meth:`~sphinxcontrib.websupport.WebSupport.get_data`::
196
197    @app.route('/docs/get_comments')
198    def get_comments():
199        username = g.user.name if g.user else None
200        moderator = g.user.moderator if g.user else False
201        node_id = request.args.get('node', '')
202        data = support.get_data(node_id, username, moderator)
203        return jsonify(**data)
204
205The final function that is needed will call :meth:`~.WebSupport.process_vote`,
206and will handle user votes on comments::
207
208   @app.route('/docs/process_vote', methods=['POST'])
209   def process_vote():
210       if g.user is None:
211           abort(401)
212       comment_id = request.form.get('comment_id')
213       value = request.form.get('value')
214       if value is None or comment_id is None:
215           abort(400)
216       support.process_vote(comment_id, g.user.id, value)
217       return "success"
218
219
220Comment Moderation
221------------------
222
223By default, all comments added through :meth:`~.WebSupport.add_comment` are
224automatically displayed.  If you wish to have some form of moderation, you can
225pass the ``displayed`` keyword argument::
226
227   comment = support.add_comment(text, node_id='node_id',
228                                 parent_id='parent_id',
229                                 username=username, proposal=proposal,
230                                 displayed=False)
231
232You can then create a new view to handle the moderation of comments.  It
233will be called when a moderator decides a comment should be accepted and
234displayed::
235
236   @app.route('/docs/accept_comment', methods=['POST'])
237   def accept_comment():
238       moderator = g.user.moderator if g.user else False
239       comment_id = request.form.get('id')
240       support.accept_comment(comment_id, moderator=moderator)
241       return 'OK'
242
243Rejecting comments happens via comment deletion.
244
245To perform a custom action (such as emailing a moderator) when a new comment is
246added but not displayed, you can pass callable to the :class:`~.WebSupport`
247class when instantiating your support object::
248
249   def moderation_callback(comment):
250       """Do something..."""
251
252   support = WebSupport(..., moderation_callback=moderation_callback)
253
254The moderation callback must take one argument, which will be the same comment
255dict that is returned by :meth:`add_comment`.
256