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