Make websupport builder inherit from serializing builder, remove separate WebSupportApp.

This commit is contained in:
Georg Brandl
2010-11-20 17:41:20 +01:00
parent 47bc250f19
commit 2ab934b232
9 changed files with 175 additions and 155 deletions

View File

@@ -7,7 +7,7 @@
:license: BSD, see LICENSE for details.
"""
import os
import pickle
import cPickle as pickle
from docutils.utils import Reporter

View File

@@ -9,31 +9,43 @@
:license: BSD, see LICENSE for details.
"""
import cPickle as pickle
from os import path
import posixpath
import shutil
from docutils.io import StringOutput
from sphinx.jinja2glue import BuiltinTemplateLoader
from sphinx.util.osutil import os_path, relative_uri, ensuredir, copyfile
from sphinx.util.jsonimpl import dumps as dump_json
from sphinx.util.websupport import is_commentable
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.builders.html import PickleHTMLBuilder
from sphinx.builders.versioning import VersioningBuilderMixin
from sphinx.writers.websupport import WebSupportTranslator
class WebSupportBuilder(StandaloneHTMLBuilder, VersioningBuilderMixin):
class WebSupportBuilder(PickleHTMLBuilder, VersioningBuilderMixin):
"""
Builds documents for the web support package.
"""
name = 'websupport'
out_suffix = '.fpickle'
def init(self):
StandaloneHTMLBuilder.init(self)
PickleHTMLBuilder.init(self)
VersioningBuilderMixin.init(self)
# templates are needed for this builder, but the serializing
# builder does not initialize them
self.init_templates()
if not isinstance(self.templates, BuiltinTemplateLoader):
raise RuntimeError('websupport builder must be used with '
'the builtin templates')
# add our custom JS
self.script_files.append('_static/websupport.js')
def set_webinfo(self, staticdir, virtual_staticdir, search, storage):
self.staticdir = staticdir
self.virtual_staticdir = virtual_staticdir
self.search = search
self.storage = storage
def init_translator_class(self):
self.translator_class = WebSupportTranslator
@@ -46,9 +58,9 @@ class WebSupportBuilder(StandaloneHTMLBuilder, VersioningBuilderMixin):
self.cur_docname = docname
self.secnumbers = self.env.toc_secnumbers.get(docname, {})
self.imgpath = '/' + posixpath.join(self.app.staticdir, '_images')
self.imgpath = '/' + posixpath.join(self.virtual_staticdir, '_images')
self.post_process_images(doctree)
self.dlpath = '/' + posixpath.join(self.app.staticdir, '_downloads')
self.dlpath = '/' + posixpath.join(self.virtual_staticdir, '_downloads')
self.docwriter.write(doctree, destination)
self.docwriter.assemble_parts()
body = self.docwriter.parts['fragment']
@@ -58,11 +70,8 @@ class WebSupportBuilder(StandaloneHTMLBuilder, VersioningBuilderMixin):
self.index_page(docname, doctree, ctx.get('title', ''))
self.handle_page(docname, ctx, event_arg=doctree)
def get_target_uri(self, docname, typ=None):
return docname
def load_indexer(self, docnames):
self.indexer = self.app.search
self.indexer = self.search
self.indexer.init_indexing(changed=docnames)
def handle_page(self, pagename, addctx, templatename='page.html',
@@ -75,11 +84,13 @@ class WebSupportBuilder(StandaloneHTMLBuilder, VersioningBuilderMixin):
def pathto(otheruri, resource=False,
baseuri=self.get_target_uri(pagename)):
if not resource:
if resource and '://' in otheruri:
return otheruri
elif not resource:
otheruri = self.get_target_uri(otheruri)
return relative_uri(baseuri, otheruri) or '#'
else:
return '/' + posixpath.join(self.app.staticdir, otheruri)
return '/' + posixpath.join(self.virtual_staticdir, otheruri)
ctx['pathto'] = pathto
ctx['hasdoc'] = lambda name: name in self.env.all_docs
ctx['encoding'] = self.config.html_output_encoding
@@ -90,47 +101,41 @@ class WebSupportBuilder(StandaloneHTMLBuilder, VersioningBuilderMixin):
self.app.emit('html-page-context', pagename, templatename,
ctx, event_arg)
# Create a dict that will be pickled and used by webapps.
css = '<link rel="stylesheet" href="%s" type=text/css />' % \
pathto('_static/pygments.css', 1)
doc_ctx = {'body': ctx.get('body', ''),
'title': ctx.get('title', ''),
'css': css,
'js': self._make_js(ctx)}
# Partially render the html template to proved a more useful ctx.
# create a dict that will be pickled and used by webapps
doc_ctx = {
'body': ctx.get('body', ''),
'title': ctx.get('title', ''),
}
# partially render the html template to get at interesting macros
template = self.templates.environment.get_template(templatename)
template_module = template.make_module(ctx)
if hasattr(template_module, 'sidebar'):
doc_ctx['sidebar'] = template_module.sidebar()
if hasattr(template_module, 'relbar'):
doc_ctx['relbar'] = template_module.relbar()
for item in ['sidebar', 'relbar', 'script', 'css']:
if hasattr(template_module, item):
doc_ctx[item] = getattr(template_module, item)()
if not outfilename:
outfilename = path.join(self.outdir, 'pickles',
os_path(pagename) + self.out_suffix)
ensuredir(path.dirname(outfilename))
f = open(outfilename, 'wb')
try:
pickle.dump(doc_ctx, f, pickle.HIGHEST_PROTOCOL)
finally:
f.close()
self.dump_context(doc_ctx, outfilename)
# if there is a source file, copy the source file for the
# "show source" link
if ctx.get('sourcename'):
source_name = path.join(self.app.builddir, self.app.staticdir,
source_name = path.join(self.staticdir,
'_sources', os_path(ctx['sourcename']))
ensuredir(path.dirname(source_name))
copyfile(self.env.doc2path(pagename), source_name)
def handle_finish(self):
StandaloneHTMLBuilder.handle_finish(self)
PickleHTMLBuilder.handle_finish(self)
VersioningBuilderMixin.finish(self)
# move static stuff over to separate directory
directories = ['_images', '_static']
for directory in directories:
src = path.join(self.outdir, directory)
dst = path.join(self.app.builddir, self.app.staticdir, directory)
dst = path.join(self.staticdir, directory)
if path.isdir(src):
if path.isdir(dst):
shutil.rmtree(dst)
@@ -138,23 +143,3 @@ class WebSupportBuilder(StandaloneHTMLBuilder, VersioningBuilderMixin):
def dump_search_index(self):
self.indexer.finish_indexing()
def _make_js(self, ctx):
def make_script(file):
path = ctx['pathto'](file, 1)
return '<script type="text/javascript" src="%s"></script>' % path
opts = {
'URL_ROOT': ctx.get('url_root', ''),
'VERSION': ctx['release'],
'COLLAPSE_INDEX': False,
'FILE_SUFFIX': '',
'HAS_SOURCE': ctx['has_source']
}
scripts = [make_script(file) for file in ctx['script_files']]
scripts.append(make_script('_static/websupport.js'))
return '\n'.join([
'<script type="text/javascript">'
'var DOCUMENTATION_OPTIONS = %s;' % dump_json(opts),
'</script>'
] + scripts)

View File

@@ -16,7 +16,13 @@
{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and
(sidebars != []) %}
{%- set url_root = pathto('', 1) %}
{# XXX necessary? #}
{%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
{%- if not embedded and docstitle %}
{%- set titlesuffix = " &mdash; "|safe + docstitle|e %}
{%- else %}
{%- set titlesuffix = "" %}
{%- endif %}
{%- macro relbar() %}
<div class="related">
@@ -78,24 +84,7 @@
{%- endif %}
{%- endmacro %}
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset={{ encoding }}" />
{{ metatags }}
{%- if not embedded and docstitle %}
{%- set titlesuffix = " &mdash; "|safe + docstitle|e %}
{%- else %}
{%- set titlesuffix = "" %}
{%- endif %}
{%- block htmltitle %}
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
{%- endblock %}
<link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
<link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
{%- for cssfile in css_files %}
<link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
{%- endfor %}
{%- if not embedded %}
{%- macro script() %}
<script type="text/javascript">
var DOCUMENTATION_OPTIONS = {
URL_ROOT: '{{ url_root }}',
@@ -108,6 +97,26 @@
{%- for scriptfile in script_files %}
<script type="text/javascript" src="{{ pathto(scriptfile, 1) }}"></script>
{%- endfor %}
{%- endmacro %}
{%- macro css() %}
<link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" />
<link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
{%- for cssfile in css_files %}
<link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
{%- endfor %}
{%- endmacro %}
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset={{ encoding }}" />
{{ metatags }}
{%- block htmltitle %}
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
{%- endblock %}
{{ css() }}
{%- if not embedded %}
{{ script() }}
{%- if use_opensearch %}
<link rel="search" type="application/opensearchdescription+xml"
title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"

View File

@@ -1,6 +1,6 @@
{#
basic/searchresults.html
~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~
Template for the body of the search results page.
@@ -17,20 +17,20 @@
<input type="submit" value="search" />
<span id="search-progress" style="padding-left: 10px"></span>
</form>
{% if search_performed %}
<h2>Search Results</h2>
{% if not search_results %}
<p>Your search did not match any results.</p>
{% endif %}
{% endif %}
{%- if search_performed %}
<h2>Search Results</h2>
{%- if not search_results %}
<p>Your search did not match any results.</p>
{%- endif %}
{%- endif %}
<div id="search-results">
{% if search_results %}
{%- if search_results %}
<ul class="search">
{% for href, caption, context in search_results %}
<li><a href="{{ href }}?highlight={{ q }}">{{ caption }}</a>
<li><a href="{{ docroot }}{{ href }}/?highlight={{ q }}">{{ caption }}</a>
<div class="context">{{ context|e }}</div>
</li>
{% endfor %}
</ul>
{% endif %}
{%- endif %}
</div>

View File

@@ -214,10 +214,18 @@
* Add a comment via ajax and insert the comment into the comment tree.
*/
function addComment(form) {
// Disable the form that is being submitted.
form.find('textarea,input').attr('disabled', 'disabled');
var node_id = form.find('input[name="node"]').val();
var parent_id = form.find('input[name="parent"]').val();
var text = form.find('textarea[name="comment"]').val();
var proposal = form.find('textarea[name="proposal"]').val();
if (text == '') {
showError('Please enter a comment.');
return;
}
// Disable the form that is being submitted.
form.find('textarea,input').attr('disabled', 'disabled');
// Send the comment to the server.
$.ajax({
@@ -227,8 +235,8 @@
data: {
node: node_id,
parent: parent_id,
text: form.find('textarea[name="comment"]').val(),
proposal: form.find('textarea[name="proposal"]').val()
text: text,
proposal: proposal
},
success: function(data, textStatus, error) {
// Reset the form.
@@ -311,7 +319,7 @@
$('#cm' + id).fadeOut('fast');
},
error: function(request, textStatus, error) {
showError("Oops, there was a problem accepting the comment.");
showError('Oops, there was a problem accepting the comment.');
}
});
}
@@ -328,7 +336,7 @@
});
},
error: function(request, textStatus, error) {
showError("Oops, there was a problem rejecting the comment.");
showError('Oops, there was a problem rejecting the comment.');
}
});
}
@@ -354,7 +362,7 @@
div.data('comment', comment);
},
error: function(request, textStatus, error) {
showError("Oops, there was a problem deleting the comment.");
showError('Oops, there was a problem deleting the comment.');
}
});
}
@@ -395,7 +403,7 @@
var classes = link.attr('class').split(/\s+/);
for (var i=0; i<classes.length; i++) {
if (classes[i] != 'sort-option') {
by = classes[i];
by = classes[i].substring(2);
}
}
setComparator();
@@ -464,7 +472,7 @@
url: opts.processVoteURL,
data: d,
error: function(request, textStatus, error) {
showError("Oops, there was a problem casting that vote.");
showError('Oops, there was a problem casting that vote.');
}
});
}
@@ -589,7 +597,8 @@
function showError(message) {
$(document.createElement('div')).attr({'class': 'popup-error'})
.append($(document.createElement('h1')).text(message))
.append($(document.createElement('div'))
.attr({'class': 'error-header'}).text(message))
.appendTo('body')
.fadeIn("slow")
.delay(2000)
@@ -642,12 +651,12 @@
};
var opts = jQuery.extend({
processVoteURL: '/process_vote',
addCommentURL: '/add_comment',
getCommentsURL: '/get_comments',
acceptCommentURL: '/accept_comment',
rejectCommentURL: '/reject_comment',
deleteCommentURL: '/delete_comment',
processVoteURL: '/_process_vote',
addCommentURL: '/_add_comment',
getCommentsURL: '/_get_comments',
acceptCommentURL: '/_accept_comment',
rejectCommentURL: '/_reject_comment',
deleteCommentURL: '/_delete_comment',
commentImage: '/static/_static/comment.png',
closeCommentImage: '/static/_static/comment-close.png',
loadingImage: '/static/_static/ajax-loader.gif',
@@ -727,7 +736,7 @@
var popupTemplate = '\
<div class="sphinx-comments" id="sc<%id%>">\
<h1>Comments</h1>\
<div class="comment-header">Comments</div>\
<form method="post" id="cf<%id%>" class="comment-form" action="/docs/add_comment">\
<textarea name="comment" cols="80"></textarea>\
<p class="propose-button">\
@@ -744,12 +753,12 @@
<input type="hidden" name="parent" value="" />\
<p class="sort-options">\
Sort by:\
<a href="#" class="sort-option rating">top</a>\
<a href="#" class="sort-option ascage">newest</a>\
<a href="#" class="sort-option age">oldest</a>\
<a href="#" class="sort-option byrating">top</a>\
<a href="#" class="sort-option byascage">newest</a>\
<a href="#" class="sort-option byage">oldest</a>\
</p>\
</form>\
<h3 id="cn<%id%>">loading comments... <img src="<%loadingImage%>" alt="" /></h3>\
<div class="comment-loading" id="cn<%id%>">loading comments... <img src="<%loadingImage%>" alt="" /></div>\
<ul id="cl<%id%>" class="comment-ul"></ul>\
</div>';

View File

@@ -24,29 +24,35 @@ from sphinx.websupport.search import BaseSearch, SEARCH_ADAPTERS
from sphinx.websupport.storage import StorageBackend
class WebSupportApp(Sphinx):
def __init__(self, *args, **kwargs):
self.staticdir = kwargs.pop('staticdir', None)
self.builddir = kwargs.pop('builddir', None)
self.search = kwargs.pop('search', None)
self.storage = kwargs.pop('storage', None)
Sphinx.__init__(self, *args, **kwargs)
class WebSupport(object):
"""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='', builddir='', datadir='', search=None,
storage=None, status=sys.stdout, warning=sys.stderr,
moderation_callback=None, staticdir='static',
docroot=''):
def __init__(self,
srcdir=None, # only required for building
builddir='', # the dir with data/static/doctrees subdirs
datadir=None, # defaults to builddir/data
staticdir=None, # defaults to builddir/static
doctreedir=None, # defaults to builddir/doctrees
search=None, # defaults to no search
storage=None, # defaults to SQLite in datadir
status=sys.stdout,
warning=sys.stderr,
moderation_callback=None,
docroot='',
staticroot='static',
):
# directories
self.srcdir = srcdir
self.builddir = builddir
self.outdir = path.join(builddir, 'data')
self.datadir = datadir or self.outdir
self.staticdir = staticdir.strip('/')
self.staticdir = staticdir or path.join(self.builddir, 'static')
self.doctreedir = staticdir or path.join(self.builddir, 'doctrees')
# web server virtual paths
self.staticroot = staticroot.strip('/')
self.docroot = docroot.strip('/')
self.status = status
self.warning = warning
self.moderation_callback = moderation_callback
@@ -55,6 +61,8 @@ class WebSupport(object):
self._init_search(search)
self._init_storage(storage)
self._globalcontext = None
self._make_base_comment_options()
def _init_storage(self, storage):
@@ -103,19 +111,27 @@ class WebSupport(object):
It will also save node data to the database.
"""
if not self.srcdir:
raise errors.SrcdirNotSpecifiedError( \
'No srcdir associated with WebSupport object')
doctreedir = path.join(self.outdir, 'doctrees')
app = WebSupportApp(self.srcdir, self.srcdir,
self.outdir, doctreedir, 'websupport',
search=self.search, status=self.status,
warning=self.warning, storage=self.storage,
staticdir=self.staticdir, builddir=self.builddir)
raise RuntimeError('No srcdir associated with WebSupport object')
app = Sphinx(self.srcdir, self.srcdir, self.outdir, self.doctreedir,
'websupport', status=self.status, warning=self.warning)
app.builder.set_webinfo(self.staticdir, self.staticroot,
self.search, self.storage)
self.storage.pre_build()
app.build()
self.storage.post_build()
def get_globalcontext(self):
"""Load and return the "global context" pickle."""
if not self._globalcontext:
infilename = path.join(self.datadir, 'globalcontext.pickle')
f = open(infilename, 'rb')
try:
self._globalcontext = pickle.load(f)
finally:
f.close()
return self._globalcontext
def get_document(self, docname, username='', moderator=False):
"""Load and return a document from a pickle. The document will
be a dict object which can be used to render a template::
@@ -146,28 +162,35 @@ class WebSupport(object):
* **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
* **script**: 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.
"""
infilename = path.join(self.datadir, 'pickles', docname + '.fpickle')
docpath = path.join(self.datadir, 'pickles', docname)
if path.isdir(docpath):
infilename = docpath + '/index.fpickle'
else:
infilename = docpath + '.fpickle'
try:
f = open(infilename, 'rb')
except IOError:
raise errors.DocumentNotFoundError(
'The document "%s" could not be found' % docname)
try:
document = pickle.load(f)
finally:
f.close()
document = pickle.load(f)
comment_opts = self._make_comment_options(username, moderator)
comment_metadata = self.storage.get_metadata(docname, moderator)
document['js'] = '\n'.join([comment_opts,
self._make_metadata(comment_metadata),
document['js']])
document['script'] = '\n'.join([comment_opts,
self._make_metadata(comment_metadata),
document['script']])
return document
def get_search_results(self, q):
@@ -181,9 +204,12 @@ class WebSupport(object):
:param q: the search query
"""
results = self.search.query(q)
ctx = {'search_performed': True,
'search_results': results,
'q': q}
ctx = {
'q': q,
'search_performed': True,
'search_results': results,
'docroot': '../', # XXX
}
document = self.get_document('search')
document['body'] = self.results_template.render(ctx)
document['title'] = 'Search Results'
@@ -359,17 +385,17 @@ class WebSupport(object):
if self.docroot != '':
comment_urls = [
('addCommentURL', 'add_comment'),
('getCommentsURL', 'get_comments'),
('processVoteURL', 'process_vote'),
('acceptCommentURL', 'accept_comment'),
('rejectCommentURL', 'reject_comment'),
('deleteCommentURL', 'delete_comment')
('addCommentURL', '_add_comment'),
('getCommentsURL', '_get_comments'),
('processVoteURL', '_process_vote'),
('acceptCommentURL', '_accept_comment'),
('rejectCommentURL', '_reject_comment'),
('deleteCommentURL', '_delete_comment')
]
for key, value in comment_urls:
self.base_comment_opts[key] = \
'/' + posixpath.join(self.docroot, value)
if self.staticdir != 'static':
if self.staticroot != 'static':
static_urls = [
('commentImage', 'comment.png'),
('closeCommentImage', 'comment-close.png'),
@@ -382,7 +408,7 @@ class WebSupport(object):
]
for key, value in static_urls:
self.base_comment_opts[key] = \
'/' + posixpath.join(self.staticdir, '_static', value)
'/' + posixpath.join(self.staticroot, '_static', value)
def _make_comment_options(self, username, moderator):
"""Helper method to create the parts of the COMMENT_OPTIONS
@@ -391,8 +417,6 @@ class WebSupport(object):
:param username: The username of the user making the request.
:param moderator: Whether the user making the request is a moderator.
"""
# XXX parts is not used?
#parts = [self.base_comment_opts]
rv = self.base_comment_opts.copy()
if username:
rv.update({

View File

@@ -9,18 +9,11 @@
:license: BSD, see LICENSE for details.
"""
__all__ = ['DocumentNotFoundError', 'SrcdirNotSpecifiedError',
'UserNotAuthorizedError', 'CommentNotAllowedError',
'NullSearchException']
class DocumentNotFoundError(Exception):
pass
class SrcdirNotSpecifiedError(Exception):
pass
class UserNotAuthorizedError(Exception):
pass

View File

@@ -39,7 +39,7 @@ class WebSupportTranslator(HTMLTranslator):
node.attributes['classes'].append(self.comment_class)
def add_db_node(self, node):
storage = self.builder.app.storage
storage = self.builder.storage
if not storage.has_node(node.uid):
storage.add_node(id=node.uid,
document=self.builder.cur_docname,

View File

@@ -65,7 +65,7 @@ class NullStorage(StorageBackend):
@with_support(storage=NullStorage())
def test_no_srcdir(support):
"""Make sure the correct exception is raised if srcdir is not given."""
raises(SrcdirNotSpecifiedError, support.build)
raises(RuntimeError, support.build)
@skip_if(sqlalchemy_missing, 'needs sqlalchemy')