Add basic web support package.

rtd2
Eric Holscher 2013-07-23 14:21:03 -07:00
parent 23ac982140
commit aa6cdd05eb
21 changed files with 1556 additions and 1 deletions

View File

@ -0,0 +1,216 @@
div#login {
margin: 1em;
background-color: white;
border: 1px solid #ccc;
padding: 0.75em 1em;
float: right;
}
div.flashed-messages {
padding: 0.5em 0;
}
div.flashed-message {
margin-bottom: 0.5em;
padding: 0.5em;
background-color: #eee;
border: 1px solid #aaa;
font-weight: bold;
}
p.error {
color: #783232;
padding: 10px;
background: #EBCACA;
}
input[name="openid"] {
background: url(openid.png) 4px no-repeat;
padding-left: 24px;
}
div.popup-error {
background: #EBCACA;
color: #783232;
display: none;
position: fixed;
padding: 0.5em;
margin: 0;
width: 100%;
top: 0;
left: 0;
z-index: 32;
border-bottom: 2px solid #AAA;
}
div.popup-error div.error-message {
font-size: 130%;
}
a.sphinx-comment img, a.sphinx-comment-close img {
margin-left: .25em;
}
a.sphinx-comment.nocomment {
visibility: hidden;
}
:hover > a.sphinx-comment {
visibility: visible;
}
div.sphinx-comments {
display: none;
max-height: 500px;
overflow: auto;
border: 1px solid #CCC;
z-index: 16;
margin-top: .5em;
padding: 6px 18px;
background: #FFF;
text-align: left;
}
div.sphinx-comments div.comment-header {
font-size: 130%;
margin-bottom: 10px;
}
div.sphinx-comments div.comment-loading {
text-align: center;
color: #11557C;
}
div.sphinx-comments textarea {
display: block;
line-height: 1.5em;
margin: 6px 0 0 0;
}
p.propose-button {
margin: 0;
}
form.comment-form input {
margin-top: .5em;
}
a.hide-propose-change {
display: none;
}
a.show-propose-change, a.hide-propose-change {
font-size: smaller;
}
p.sort-options {
float: right;
margin: 0 2em;
}
a.sort-option {
margin-left: .5em;
}
a.sel {
color: #2491cf;
text-decoration: underline;
}
div.sphinx-comments ul {
list-style: none;
padding: 0 0 0 10px;
}
div.sphinx-comments ul li {
margin-top: .5em;
}
div.vote {
padding-top: 3px;
float: left;
}
div.arrow {
width: 16px;
height: 16px;
margin-bottom: 10px;
}
div.comment-content p, pre.proposal {
line-height: 1.5em;
margin: 0 0 0 25px;
}
pre.proposal {
color: #555;
display: none;
margin: .5em 0 .5em 25px;
}
a.hide-proposal, a.show-proposal {
display: none;
}
pre.proposal .prop-added {
background-color: #CFC;
}
pre.proposal .prop-removed {
background-color: #FCC;
}
pre.proposal ins {
text-decoration: none;
background-color: #8F8;
}
pre.proposal del {
text-decoration: none;
background-color: #F88;
}
.rating, .delta {
font-size: .9em;
}
a.un {
display: none;
}
.user-id {
color: #EE9816;
}
p.tagline span {
margin-right: .25em;
}
p.comment-opts a {
color: #666;
font-size: .9em;
margin-right: 1em;
}
ul.children {
margin-left: 22px;
border-left: 1px solid #DDD;
}
a.close-reply {
display: none;
}
div.clearleft {
clear: left;
}
.hidden {
display: none;
}
a.oid img {
padding: 5px;
border: 1px solid #DDD;
margin: 1em .5em 0;
}

View File

@ -0,0 +1,853 @@
/*
* websupport.js
* ~~~~~~~~~~~~~
*
* sphinx.websupport utilties for all documentation.
*
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
* :license: BSD, see LICENSE for details.
*
*/
(function($) {
$.fn.autogrow = function() {
return this.each(function() {
var textarea = this;
$.fn.autogrow.resize(textarea);
$(textarea)
.focus(function() {
textarea.interval = setInterval(function() {
$.fn.autogrow.resize(textarea);
}, 500);
})
.blur(function() {
clearInterval(textarea.interval);
});
});
};
$.fn.autogrow.resize = function(textarea) {
var lineHeight = parseInt($(textarea).css('line-height'), 10);
var lines = textarea.value.split('\n');
var columns = textarea.cols;
var lineCount = 0;
$.each(lines, function() {
lineCount += Math.ceil(this.length / columns) || 1;
});
var height = lineHeight * (lineCount + 1);
$(textarea).css('height', height);
};
})(jQuery);
(function($) {
var comp, by;
function init() {
initEvents();
initComparator();
initOptions();
initMetaData();
}
function initMetaData() {
$.ajax({
type: 'GET',
url: opts.metadataURL,
data: {document: window.location.pathname.substring(1)},
success: function(data, textStatus, request) {
COMMENT_METADATA = data
},
error: function(request, textStatus, error) {
showError('Oops, there was a problem retrieving comment metadata.');
},
dataType: 'json'
});
}
function initOptions() {
$.ajax({
type: 'GET',
url: opts.optionsURL,
//data: {},
success: function(data, textStatus, request) {
COMMENT_OPTIONS = data
},
error: function(request, textStatus, error) {
showError('Oops, there was a problem retrieving the comment options.');
},
dataType: 'json'
});
}
function initEvents() {
$('a.comment-close').live("click", function(event) {
event.preventDefault();
hide($(this).attr('id').substring(2));
});
$('a.vote').live("click", function(event) {
event.preventDefault();
handleVote($(this));
});
$('a.reply').live("click", function(event) {
event.preventDefault();
openReply($(this).attr('id').substring(2));
});
$('a.close-reply').live("click", function(event) {
event.preventDefault();
closeReply($(this).attr('id').substring(2));
});
$('a.sort-option').live("click", function(event) {
event.preventDefault();
handleReSort($(this));
});
$('a.show-proposal').live("click", function(event) {
event.preventDefault();
showProposal($(this).attr('id').substring(2));
});
$('a.hide-proposal').live("click", function(event) {
event.preventDefault();
hideProposal($(this).attr('id').substring(2));
});
$('a.show-propose-change').live("click", function(event) {
event.preventDefault();
showProposeChange($(this).attr('id').substring(2));
});
$('a.hide-propose-change').live("click", function(event) {
event.preventDefault();
hideProposeChange($(this).attr('id').substring(2));
});
$('a.accept-comment').live("click", function(event) {
event.preventDefault();
acceptComment($(this).attr('id').substring(2));
});
$('a.delete-comment').live("click", function(event) {
event.preventDefault();
deleteComment($(this).attr('id').substring(2));
});
$('a.comment-markup').live("click", function(event) {
event.preventDefault();
toggleCommentMarkupBox($(this).attr('id').substring(2));
});
}
/**
* Set comp, which is a comparator function used for sorting and
* inserting comments into the list.
*/
function setComparator() {
// If the first three letters are "asc", sort in ascending order
// and remove the prefix.
if (by.substring(0,3) == 'asc') {
var i = by.substring(3);
comp = function(a, b) { return a[i] - b[i]; };
} else {
// Otherwise sort in descending order.
comp = function(a, b) { return b[by] - a[by]; };
}
// Reset link styles and format the selected sort option.
$('a.sel').attr('href', '#').removeClass('sel');
$('a.by' + by).removeAttr('href').addClass('sel');
}
/**
* Create a comp function. If the user has preferences stored in
* the sortBy cookie, use those, otherwise use the default.
*/
function initComparator() {
by = 'rating'; // Default to sort by rating.
// If the sortBy cookie is set, use that instead.
if (document.cookie.length > 0) {
var start = document.cookie.indexOf('sortBy=');
if (start != -1) {
start = start + 7;
var end = document.cookie.indexOf(";", start);
if (end == -1) {
end = document.cookie.length;
by = unescape(document.cookie.substring(start, end));
}
}
}
setComparator();
}
/**
* Show a comment div.
*/
function show(id) {
$('#ao' + id).hide();
$('#ah' + id).show();
var context = $.extend({id: id}, opts);
var popup = $(renderTemplate(popupTemplate, context)).hide();
popup.find('textarea[name="proposal"]').hide();
popup.find('a.by' + by).addClass('sel');
var form = popup.find('#cf' + id);
form.submit(function(event) {
event.preventDefault();
addComment(form);
});
$('#s' + id).after(popup);
popup.slideDown('fast', function() {
getComments(id);
});
}
/**
* Hide a comment div.
*/
function hide(id) {
$('#ah' + id).hide();
$('#ao' + id).show();
var div = $('#sc' + id);
div.slideUp('fast', function() {
div.remove();
});
}
/**
* Perform an ajax request to get comments for a node
* and insert the comments into the comments tree.
*/
function getComments(id) {
$.ajax({
type: 'GET',
url: opts.getCommentsURL,
data: {node: id},
success: function(data, textStatus, request) {
var ul = $('#cl' + id);
var speed = 100;
$('#cf' + id)
.find('textarea[name="proposal"]')
.data('source', data.source);
if (data.comments.length === 0) {
ul.html('<li>No comments yet.</li>');
ul.data('empty', true);
} else {
// If there are comments, sort them and put them in the list.
var comments = sortComments(data.comments);
speed = data.comments.length * 100;
appendComments(comments, ul);
ul.data('empty', false);
}
$('#cn' + id).slideUp(speed + 200);
ul.slideDown(speed);
},
error: function(request, textStatus, error) {
showError('Oops, there was a problem retrieving the comments.');
},
dataType: 'json'
});
}
/**
* Add a comment via ajax and insert the comment into the comment tree.
*/
function addComment(form) {
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({
type: "POST",
url: opts.addCommentURL,
dataType: 'json',
data: {
node: node_id,
parent: parent_id,
text: text,
proposal: proposal
},
success: function(data, textStatus, error) {
// Reset the form.
if (node_id) {
hideProposeChange(node_id);
}
form.find('textarea')
.val('')
.add(form.find('input'))
.removeAttr('disabled');
var ul = $('#cl' + (node_id || parent_id));
if (ul.data('empty')) {
$(ul).empty();
ul.data('empty', false);
}
insertComment(data.comment);
var ao = $('#ao' + node_id);
ao.find('img').attr({'src': opts.commentBrightImage});
if (node_id) {
// if this was a "root" comment, remove the commenting box
// (the user can get it back by reopening the comment popup)
$('#ca' + node_id).slideUp();
}
},
error: function(request, textStatus, error) {
form.find('textarea,input').removeAttr('disabled');
showError('Oops, there was a problem adding the comment.');
}
});
}
/**
* Recursively append comments to the main comment list and children
* lists, creating the comment tree.
*/
function appendComments(comments, ul) {
$.each(comments, function() {
var div = createCommentDiv(this);
ul.append($(document.createElement('li')).html(div));
appendComments(this.children, div.find('ul.comment-children'));
// To avoid stagnating data, don't store the comments children in data.
this.children = null;
div.data('comment', this);
});
}
/**
* After adding a new comment, it must be inserted in the correct
* location in the comment tree.
*/
function insertComment(comment) {
var div = createCommentDiv(comment);
// To avoid stagnating data, don't store the comments children in data.
comment.children = null;
div.data('comment', comment);
var ul = $('#cl' + (comment.node || comment.parent));
var siblings = getChildren(ul);
var li = $(document.createElement('li'));
li.hide();
// Determine where in the parents children list to insert this comment.
for(i=0; i < siblings.length; i++) {
if (comp(comment, siblings[i]) <= 0) {
$('#cd' + siblings[i].id)
.parent()
.before(li.html(div));
li.slideDown('fast');
return;
}
}
// If we get here, this comment rates lower than all the others,
// or it is the only comment in the list.
ul.append(li.html(div));
li.slideDown('fast');
}
function acceptComment(id) {
$.ajax({
type: 'POST',
url: opts.acceptCommentURL,
data: {id: id},
success: function(data, textStatus, request) {
$('#cm' + id).fadeOut('fast');
$('#cd' + id).removeClass('moderate');
},
error: function(request, textStatus, error) {
showError('Oops, there was a problem accepting the comment.');
}
});
}
function deleteComment(id) {
$.ajax({
type: 'POST',
url: opts.deleteCommentURL,
data: {id: id},
success: function(data, textStatus, request) {
var div = $('#cd' + id);
if (data == 'delete') {
// Moderator mode: remove the comment and all children immediately
div.slideUp('fast', function() {
div.remove();
});
return;
}
// User mode: only mark the comment as deleted
div
.find('span.user-id:first')
.text('[deleted]').end()
.find('div.comment-text:first')
.text('[deleted]').end()
.find('#cm' + id + ', #dc' + id + ', #ac' + id + ', #rc' + id +
', #sp' + id + ', #hp' + id + ', #cr' + id + ', #rl' + id)
.remove();
var comment = div.data('comment');
comment.username = '[deleted]';
comment.text = '[deleted]';
div.data('comment', comment);
},
error: function(request, textStatus, error) {
showError('Oops, there was a problem deleting the comment.');
}
});
}
function showProposal(id) {
$('#sp' + id).hide();
$('#hp' + id).show();
$('#pr' + id).slideDown('fast');
}
function hideProposal(id) {
$('#hp' + id).hide();
$('#sp' + id).show();
$('#pr' + id).slideUp('fast');
}
function showProposeChange(id) {
$('#pc' + id).hide();
$('#hc' + id).show();
var textarea = $('#pt' + id);
textarea.val(textarea.data('source'));
$.fn.autogrow.resize(textarea[0]);
textarea.slideDown('fast');
}
function hideProposeChange(id) {
$('#hc' + id).hide();
$('#pc' + id).show();
var textarea = $('#pt' + id);
textarea.val('').removeAttr('disabled');
textarea.slideUp('fast');
}
function toggleCommentMarkupBox(id) {
$('#mb' + id).toggle();
}
/** Handle when the user clicks on a sort by link. */
function handleReSort(link) {
var classes = link.attr('class').split(/\s+/);
for (var i=0; i<classes.length; i++) {
if (classes[i] != 'sort-option') {
by = classes[i].substring(2);
}
}
setComparator();
// Save/update the sortBy cookie.
var expiration = new Date();
expiration.setDate(expiration.getDate() + 365);
document.cookie= 'sortBy=' + escape(by) +
';expires=' + expiration.toUTCString();
$('ul.comment-ul').each(function(index, ul) {
var comments = getChildren($(ul), true);
comments = sortComments(comments);
appendComments(comments, $(ul).empty());
});
}
/**
* Function to process a vote when a user clicks an arrow.
*/
function handleVote(link) {
if (!opts.voting) {
showError("You'll need to login to vote.");
return;
}
var id = link.attr('id');
if (!id) {
// Didn't click on one of the voting arrows.
return;
}
// If it is an unvote, the new vote value is 0,
// Otherwise it's 1 for an upvote, or -1 for a downvote.
var value = 0;
if (id.charAt(1) != 'u') {
value = id.charAt(0) == 'u' ? 1 : -1;
}
// The data to be sent to the server.
var d = {
comment_id: id.substring(2),
value: value
};
// Swap the vote and unvote links.
link.hide();
$('#' + id.charAt(0) + (id.charAt(1) == 'u' ? 'v' : 'u') + d.comment_id)
.show();
// The div the comment is displayed in.
var div = $('div#cd' + d.comment_id);
var data = div.data('comment');
// If this is not an unvote, and the other vote arrow has
// already been pressed, unpress it.
if ((d.value !== 0) && (data.vote === d.value * -1)) {
$('#' + (d.value == 1 ? 'd' : 'u') + 'u' + d.comment_id).hide();
$('#' + (d.value == 1 ? 'd' : 'u') + 'v' + d.comment_id).show();
}
// Update the comments rating in the local data.
data.rating += (data.vote === 0) ? d.value : (d.value - data.vote);
data.vote = d.value;
div.data('comment', data);
// Change the rating text.
div.find('.rating:first')
.text(data.rating + ' point' + (data.rating == 1 ? '' : 's'));
// Send the vote information to the server.
$.ajax({
type: "POST",
url: opts.processVoteURL,
data: d,
error: function(request, textStatus, error) {
showError('Oops, there was a problem casting that vote.');
}
});
}
/**
* Open a reply form used to reply to an existing comment.
*/
function openReply(id) {
// Swap out the reply link for the hide link
$('#rl' + id).hide();
$('#cr' + id).show();
// Add the reply li to the children ul.
var div = $(renderTemplate(replyTemplate, {id: id})).hide();
$('#cl' + id)
.prepend(div)
// Setup the submit handler for the reply form.
.find('#rf' + id)
.submit(function(event) {
event.preventDefault();
addComment($('#rf' + id));
closeReply(id);
})
.find('input[type=button]')
.click(function() {
closeReply(id);
});
div.slideDown('fast', function() {
$('#rf' + id).find('textarea').focus();
});
}
/**
* Close the reply form opened with openReply.
*/
function closeReply(id) {
// Remove the reply div from the DOM.
$('#rd' + id).slideUp('fast', function() {
$(this).remove();
});
// Swap out the hide link for the reply link
$('#cr' + id).hide();
$('#rl' + id).show();
}
/**
* Recursively sort a tree of comments using the comp comparator.
*/
function sortComments(comments) {
comments.sort(comp);
$.each(comments, function() {
this.children = sortComments(this.children);
});
return comments;
}
/**
* Get the children comments from a ul. If recursive is true,
* recursively include childrens' children.
*/
function getChildren(ul, recursive) {
var children = [];
ul.children().children("[id^='cd']")
.each(function() {
var comment = $(this).data('comment');
if (recursive)
comment.children = getChildren($(this).find('#cl' + comment.id), true);
children.push(comment);
});
return children;
}
/** Create a div to display a comment in. */
function createCommentDiv(comment) {
if (!comment.displayed && !opts.moderator) {
return $('<div class="moderate">Thank you! Your comment will show up '
+ 'once it is has been approved by a moderator.</div>');
}
// Prettify the comment rating.
comment.pretty_rating = comment.rating + ' point' +
(comment.rating == 1 ? '' : 's');
// Make a class (for displaying not yet moderated comments differently)
comment.css_class = comment.displayed ? '' : ' moderate';
// Create a div for this comment.
var context = $.extend({}, opts, comment);
var div = $(renderTemplate(commentTemplate, context));
// If the user has voted on this comment, highlight the correct arrow.
if (comment.vote) {
var direction = (comment.vote == 1) ? 'u' : 'd';
div.find('#' + direction + 'v' + comment.id).hide();
div.find('#' + direction + 'u' + comment.id).show();
}
if (opts.moderator || comment.text != '[deleted]') {
div.find('a.reply').show();
if (comment.proposal_diff)
div.find('#sp' + comment.id).show();
if (opts.moderator && !comment.displayed)
div.find('#cm' + comment.id).show();
if (opts.moderator || (opts.username == comment.username))
div.find('#dc' + comment.id).show();
}
return div;
}
/**
* A simple template renderer. Placeholders such as <%id%> are replaced
* by context['id'] with items being escaped. Placeholders such as <#id#>
* are not escaped.
*/
function renderTemplate(template, context) {
var esc = $(document.createElement('div'));
function handle(ph, escape) {
var cur = context;
$.each(ph.split('.'), function() {
if (cur) {
cur = cur[this];
} else {
console.log("Bad data:" + cur)
console.log("Bad data data:" + ph)
console.log("Bad data data:" + escape)
}
});
return escape ? esc.text(cur || "").html() : cur;
}
return template.replace(/<([%#])([\w\.]*)\1>/g, function() {
return handle(arguments[2], arguments[1] == '%' ? true : false);
});
}
/** Flash an error message briefly. */
function showError(message) {
$(document.createElement('div')).attr({'class': 'popup-error'})
.append($(document.createElement('div'))
.attr({'class': 'error-message'}).text(message))
.appendTo('body')
.fadeIn("slow")
.delay(2000)
.fadeOut("slow");
}
/** Add a link the user uses to open the comments popup. */
$.fn.comment = function() {
return this.each(function() {
var id = $(this).attr('id').substring(1);
var count = COMMENT_METADATA[id];
var title = count + ' comment' + (count == 1 ? '' : 's');
var image = count > 0 ? opts.commentBrightImage : opts.commentImage;
var addcls = count == 0 ? ' nocomment' : '';
$(this)
.append(
$(document.createElement('a')).attr({
href: '#',
'class': 'sphinx-comment-open' + addcls,
id: 'ao' + id
})
.append($(document.createElement('img')).attr({
src: image,
alt: 'comment',
title: title
}))
.click(function(event) {
event.preventDefault();
show($(this).attr('id').substring(2));
})
)
.append(
$(document.createElement('a')).attr({
href: '#',
'class': 'sphinx-comment-close hidden',
id: 'ah' + id
})
.append($(document.createElement('img')).attr({
src: opts.closeCommentImage,
alt: 'close',
title: 'close'
}))
.click(function(event) {
event.preventDefault();
hide($(this).attr('id').substring(2));
})
);
});
};
baseURL = "{{ websupport2_base_url }}";
var opts = {
processVoteURL: baseURL + '/_process_vote',
addCommentURL: baseURL + '/_add_comment',
getCommentsURL: baseURL + '/_get_comments',
acceptCommentURL: baseURL + '/_accept_comment',
deleteCommentURL: baseURL + '/_delete_comment',
metadataURL: baseURL + '/_get_metadata',
optionsURL: baseURL + '/_get_options',
commentImage: '/static/_static/comment.png',
closeCommentImage: '/static/_static/comment-close.png',
loadingImage: '/static/_static/ajax-loader.gif',
commentBrightImage: '/static/_static/comment-bright.png',
upArrow: '/static/_static/up.png',
downArrow: '/static/_static/down.png',
upArrowPressed: '/static/_static/up-pressed.png',
downArrowPressed: '/static/_static/down-pressed.png',
voting: false,
moderator: false
};
if (typeof COMMENT_OPTIONS != "undefined") {
opts = jQuery.extend(opts, COMMENT_OPTIONS);
}
var popupTemplate = '\
<div class="sphinx-comments" id="sc<%id%>">\
<p class="sort-options">\
Sort by:\
<a href="#" class="sort-option byrating">best rated</a>\
<a href="#" class="sort-option byascage">newest</a>\
<a href="#" class="sort-option byage">oldest</a>\
</p>\
<div class="comment-header">Comments</div>\
<div class="comment-loading" id="cn<%id%>">\
loading comments... <img src="<%loadingImage%>" alt="" /></div>\
<ul id="cl<%id%>" class="comment-ul"></ul>\
<div id="ca<%id%>">\
<p class="add-a-comment">Add a comment\
(<a href="#" class="comment-markup" id="ab<%id%>">markup</a>):</p>\
<div class="comment-markup-box" id="mb<%id%>">\
reStructured text markup: <i>*emph*</i>, <b>**strong**</b>, \
<tt>``code``</tt>, \
code blocks: <tt>::</tt> and an indented block after blank line</div>\
<form method="post" id="cf<%id%>" class="comment-form" action="">\
<textarea name="comment" cols="80"></textarea>\
<p class="propose-button">\
<a href="#" id="pc<%id%>" class="show-propose-change">\
Propose a change &#9657;\
</a>\
<a href="#" id="hc<%id%>" class="hide-propose-change">\
Propose a change &#9663;\
</a>\
</p>\
<textarea name="proposal" id="pt<%id%>" cols="80"\
spellcheck="false"></textarea>\
<input type="submit" value="Add comment" />\
<input type="hidden" name="node" value="<%id%>" />\
<input type="hidden" name="parent" value="" />\
</form>\
</div>\
</div>';
var commentTemplate = '\
<div id="cd<%id%>" class="sphinx-comment<%css_class%>">\
<div class="vote">\
<div class="arrow">\
<a href="#" id="uv<%id%>" class="vote" title="vote up">\
<img src="<%upArrow%>" />\
</a>\
<a href="#" id="uu<%id%>" class="un vote" title="vote up">\
<img src="<%upArrowPressed%>" />\
</a>\
</div>\
<div class="arrow">\
<a href="#" id="dv<%id%>" class="vote" title="vote down">\
<img src="<%downArrow%>" id="da<%id%>" />\
</a>\
<a href="#" id="du<%id%>" class="un vote" title="vote down">\
<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>\
<div class="comment-text comment"><#text#></div>\
<p class="comment-opts comment">\
<a href="#" class="reply hidden" id="rl<%id%>">reply &#9657;</a>\
<a href="#" class="close-reply" id="cr<%id%>">reply &#9663;</a>\
<a href="#" id="sp<%id%>" class="show-proposal">proposal &#9657;</a>\
<a href="#" id="hp<%id%>" class="hide-proposal">proposal &#9663;</a>\
<a href="#" id="dc<%id%>" class="delete-comment hidden">delete</a>\
<span id="cm<%id%>" class="moderation hidden">\
<a href="#" id="ac<%id%>" class="accept-comment">accept</a>\
</span>\
</p>\
<pre class="proposal" id="pr<%id%>">\
<#proposal_diff#>\
</pre>\
<ul class="comment-children" id="cl<%id%>"></ul>\
</div>\
<div class="clearleft"></div>\
</div>\
</div>';
var replyTemplate = '\
<li>\
<div class="reply-div" id="rd<%id%>">\
<form id="rf<%id%>">\
<textarea name="comment" cols="80"></textarea>\
<input type="submit" value="Add reply" />\
<input type="button" value="Cancel" />\
<input type="hidden" name="parent" value="<%id%>" />\
<input type="hidden" name="node" value="" />\
</form>\
</div>\
</li>';
$(document).ready(function() {
init();
});
})(jQuery);
$(document).ready(function() {
// add comment anchors for all paragraphs that are commentable
$('.sphinx-has-comment').comment();
// highlight search words in search results
$("div.context").each(function() {
var params = $.getQueryParameters();
var terms = (params.q) ? params.q[0].split(/\s+/) : [];
var result = $(this);
$.each(terms, function() {
result.highlightText(this.toLowerCase(), 'highlighted');
});
});
// directly open comment window if requested
var anchor = document.location.hash;
if (anchor.substring(0, 9) == '#comment-') {
$('#ao' + anchor.substring(9)).click();
document.location.hash = '#s' + anchor.substring(9);
}
});

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# From sphinx.writers.websupport
from sphinx.writers.html import HTMLTranslator
from sphinx.util.websupport import is_commentable
class UUIDTranslator(HTMLTranslator):
"""
Our custom HTML translator.
"""
def __init__(self, builder, *args, **kwargs):
HTMLTranslator.__init__(self, builder, *args, **kwargs)
self.comment_class = 'sphinx-has-comment'
def dispatch_visit(self, node):
if is_commentable(node):
self.handle_visit_commentable(node)
HTMLTranslator.dispatch_visit(self, node)
def handle_visit_commentable(self, node):
# We will place the node in the HTML id attribute. If the node
# already has an id (for indexing purposes) put an empty
# span with the existing id directly before this node's HTML.
self.add_db_node(node)
if node.attributes['ids']:
self.body.append('<span id="%s"></span>'
% node.attributes['ids'][0])
node.attributes['ids'] = ['s%s' % node.uid]
node.attributes['classes'].append(self.comment_class)
def add_db_node(self, node):
storage = self.builder.storage
if not storage.has_node(node.uid):
storage.add_node(id=node.uid,
document=self.builder.current_docname,
source=node.rawsource or node.astext())

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# from sphinx.builders.websupport
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.util import copy_static_entry
from sphinx.util.console import bold
from sphinx.util.osutil import copyfile
from translator import UUIDTranslator
from websupport.backend import DjangoStorage
import os
def copy_media(app, exception):
if app.builder.name != 'websupport2' or exception:
return
for file in ['websupport2.css', 'websupport2.js_t']:
app.info(bold('Copying %s... ' % file), nonl=True)
dest_dir = os.path.join(app.builder.outdir, '_static')
dest = os.path.join(dest_dir, file)
source = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
'_static',
file
)
ctx = app.builder.globalcontext
ctx['websupport2_base_url'] = app.builder.config.websupport2_base_url
copy_static_entry(source, dest_dir, app.builder, ctx)
#copyfile(source, dest)
app.info('done')
class UUIDBuilder(StandaloneHTMLBuilder):
"""
Builds documents for the web support package.
"""
name = 'websupport2'
versioning_method = 'commentable'
storage = DjangoStorage()
def init(self):
StandaloneHTMLBuilder.init(self)
# add our custom bits
self.script_files.append('_static/websupport2.js')
self.css_files.append('_static/websupport2.css')
def init_translator_class(self):
self.translator_class = UUIDTranslator
def setup(app):
app.add_builder(UUIDBuilder)
app.connect('build-finished', copy_media)
app.add_config_value('websupport2_base_url', 'http://localhost:8000/websupport', 'html')

View File

@ -10,11 +10,13 @@ setup_environ(settings.sqlite)
sys.path.append(os.path.abspath('_ext'))
sys.path.append(os.path.abspath('_ext/websupport2'))
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx_http_domain',
'djangodocs',
'websupport2',
]
templates_path = ['_templates']
source_suffix = '.rst'

View File

@ -2,6 +2,7 @@ from .backends import (
sphinx,
sphinx_epub,
sphinx_htmldir,
sphinx_websupport2,
sphinx_man,
sphinx_pdf,
sphinx_dash,
@ -11,6 +12,7 @@ from .backends import (
loading = {'sphinx': sphinx.Builder,
'sphinx_epub': sphinx_epub.Builder,
'sphinx_htmldir': sphinx_htmldir.Builder,
'sphinx_websupport2': sphinx_websupport2.Builder,
'sphinx_man': sphinx_man.Builder,
'sphinx_pdf': sphinx_pdf.Builder,
'sphinx_dash': sphinx_dash.Builder,

View File

@ -0,0 +1,27 @@
import logging
import os
import shutil
from doc_builder.base import restoring_chdir
from doc_builder.backends.sphinx import Builder as HtmlBuilder
from projects.utils import run
from core.utils import copy_to_app_servers
from django.conf import settings
log = logging.getLogger(__name__)
class Builder(HtmlBuilder):
@restoring_chdir
def build(self, **kwargs):
project = self.version.project
os.chdir(self.version.project.conf_dir(self.version.slug))
if project.use_virtualenv:
build_command = '%s -b websupport2 . _build/html' % project.venv_bin(
version=self.version.slug, bin='sphinx-build')
else:
build_command = "sphinx-build -b websupport2 . _build/html"
build_results = run(build_command)
if 'no targets are out of date.' in build_results[1]:
self._changed = False
return build_results

View File

@ -17,6 +17,7 @@ THEME_HAIKU = 'haiku'
DOCUMENTATION_CHOICES = (
('sphinx', _('Sphinx Html')),
('sphinx_htmldir', _('Sphinx HtmlDir')),
('sphinx_websupport2', _('Sphinx Websupport')),
#('sphinx_man', 'Sphinx Man'),
#('rdoc', 'Rdoc'),
)

View File

@ -131,6 +131,7 @@ INSTALLED_APPS = [
'builds',
'core',
'rtd_tests',
'websupport',
]
if DEBUG:

View File

@ -104,6 +104,8 @@ urlpatterns = patterns(
url(r'^mlt/(?P<project_slug>[-\w]+)/(?P<filename>.*)$',
'core.views.morelikethis',
name='morelikethis'),
url(r'^websupport/', include('websupport.urls')),
)
if settings.DEBUG:
@ -114,5 +116,5 @@ if settings.DEBUG:
{'template': 'style_catalog.html'}),
url(regex='^%s/(?P<path>.*)$' % settings.MEDIA_URL.strip('/'),
view='django.views.static.serve',
kwargs={'document_root': settings.MEDIA_ROOT})
kwargs={'document_root': settings.MEDIA_ROOT}),
)

View File

View File

@ -0,0 +1,6 @@
from django.contrib import admin
from .models import SphinxComment, SphinxNode
admin.site.register(SphinxNode)
admin.site.register(SphinxComment)

View File

@ -0,0 +1,49 @@
import json
from sphinx.websupport.storage import StorageBackend
from django.core import serializers
from .models import SphinxComment, SphinxNode
class DjangoStorage(StorageBackend):
"""
A Sphinx StorageBackend using Django.
"""
def has_node(self, id):
return SphinxNode.objects.filter(hash=id).exists()
def add_node(self, id, document, source):
created, node = SphinxNode.objects.get_or_create(hash=id, document=document, source=source)
return created
def get_metadata(self, docname, moderator=None):
ret_dict = {}
for node in SphinxNode.objects.filter(document=docname):
ret_dict[node.hash] = node.comments.count()
return ret_dict
def get_data(self, node_id, username, moderator=None):
node = SphinxNode.objects.get(hash=node_id)
ret_comments = []
for comment in node.comments.all():
json_data = json.loads(serializers.serialize("json", [comment]))[0]['fields']
json_data['children'] = []
ret_comments.append(
json_data
)
return {'source': '',
'comments': ret_comments}
def add_comment(self, text, displayed, username, time,
proposal, node_id, parent_id, moderator):
proposal_diff = None
proposal_diff_text = None
node = SphinxNode.objects.get(hash=node_id)
comment = SphinxComment.objects.create(node=node, text=text, displayed=displayed, rating=0)
data = json.loads(serializers.serialize("json", [comment]))[0]
return data

View File

@ -0,0 +1,55 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _, ugettext
from projects.models import Project
from builds.models import Version
class SphinxNode(models.Model):
"""
Original Sphinx Websupport schema:
id VARCHAR(32) NOT NULL,
document VARCHAR(256) NOT NULL,
source TEXT NOT NULL,
PRIMARY KEY (id)
"""
project = models.ForeignKey(Project, verbose_name=_('Project'),
related_name='nodes', null=True)
version = models.ForeignKey(Version, verbose_name=_('Version'),
related_name='nodes', null=True)
document = models.CharField(_('Path'), max_length=255)
hash = models.CharField(_('Hash'), max_length=255)
source = models.TextField(_('Source'))
def __unicode__(self):
return "%s on %s" % (self.hash, self.document)
class SphinxComment(models.Model):
"""
Original Sphinx Websupport schema:
rating INTEGER NOT NULL,
time DATETIME NOT NULL,
text TEXT NOT NULL,
displayed BOOLEAN,
username VARCHAR(64),
proposal TEXT,
proposal_diff TEXT,
path VARCHAR(256),
node_id VARCHAR,
PRIMARY KEY (id),
CHECK (displayed IN (0, 1)),
FOREIGN KEY(node_id) REFERENCES sphinx_nodes (id)
"""
date = models.DateTimeField(_('Date'), auto_now_add=True)
rating = models.IntegerField(_('Rating'), default=0)
# Comments
text = models.TextField(_('Text'))
displayed = models.BooleanField(_('Displayed'))
user = models.ForeignKey(User, blank=True, null=True)
node = models.ForeignKey(SphinxNode, related_name='comments')
def __unicode__(self):
return "%s - %s" % (self.text, self.node)

View File

@ -0,0 +1 @@
../../docs/_build/websupport/static

View File

@ -0,0 +1,57 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{% block title %}{% endblock %}</title>
{% block css %}
{{ sg.css|safe }}
{% endblock %}
<link rel="stylesheet" href="/static/sphinxweb.css" type="text/css" media="screen" />
{% block js %}
{{ sg.script|safe }}
{% endblock %}
</head>
<body>
{% block relbar1 %}
<div class="related">
<h3>Navigation</h3>
<ul>
<li><a href="{{ url_for('docs.index') }}">{{ sg.shorttitle }}</a> &raquo;</li>
</ul>
</div>
{% endblock %}
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body">
{% block body %}{% endblock %}
</div>
</div>
</div>
{% block sidebar %}{% endblock %}
<div class="clearer"></div>
{% block relbar2 %}{% endblock %}
{%- block footer %}
<div class="footer">
{%- if sg %}
{%- if sg.show_copyright %}
&copy; Copyright {{ sg.copyright }}.
{%- endif %}
{%- if sg.last_updated %}
Last updated on {{ sg.last_updated }}.
{%- endif %}
{%- if sg.show_sphinx %}
Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>
{{ sg.sphinx_version }}.
{%- endif %}
{%- endif %}
</div>
{%- endblock %}
</body>
</html >

View File

@ -0,0 +1,33 @@
{% extends "docbase.html" %}
{% block title %}
{{ document.title|striptags }}
{% endblock %}
{% block css %}
{{ document.css|safe }}
{% endblock %}
{% block js %}
{{ document.script|safe }}
{% endblock %}
{% block relbar1 %}
{{ document.relbar|safe }}
{% endblock %}
{% block body %}
{{ document.body|safe }}
{% endblock %}
{% block content %}
{{ document.body|safe }}
{% endblock %}
{% block sidebar %}
{{ document.sidebar|safe }}
{% endblock %}
{% block relbar2 %}
{{ document.relbar|safe }}
{% endblock %}

View File

@ -0,0 +1,57 @@
<!DOCTYPE html PUBLIC "//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta httpequiv="ContentType" content="text/html; charset=utf8" />
<title>{% block title %}{% endblock %}</title>
{% block css %}
{{ sg.css|safe }}
{% endblock %}
<link rel="stylesheet" href="/static/sphinxweb.css" type="text/css" media="screen" />
{% block js %}
{{ sg.script|safe }}
{% endblock %}
</head>
<body>
{% block relbar1 %}
<div class="related">
<h3>Navigation</h3>
<ul>
<li><a href="">{{ sg.shorttitle }}</a> &raquo;</li>
</ul>
</div>
{% endblock %}
<div class="document">
<div class="documentwrapper">
<div class="bodywrapper">
<div class="body">
{% block body %}{% endblock %}
</div>
</div>
</div>
{% block sidebar %}{% endblock %}
<div class="clearer"></div>
{% block relbar2 %}{% endblock %}
{% block footer %}
<div class="footer">
{% if sg %}
{% if sg.show_copyright %}
&copy; Copyright {{ sg.copyright }}.
{% endif %}
{% if sg.last_updated %}
Last updated on {{ sg.last_updated }}.
{% endif %}
{% if sg.show_sphinx %}
Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>
{{ sg.sphinx_version }}.
{% endif %}
{% endif %}
</div>
{% endblock %}
</body>
</html >

View File

@ -0,0 +1,16 @@
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)

View File

@ -0,0 +1,26 @@
from django.conf.urls.defaults import patterns, url
from django.conf import settings
urlpatterns = patterns(
# base view, flake8 complains if it is on the previous line.
'',
url(r'build',
'websupport.views.build',
name='build'),
url(r'_add_comment',
'websupport.views.add_comment',
name='add_comment'),
url(r'_get_comments',
'websupport.views.get_comments',
name='get_comments'),
url(r'_get_metadata',
'websupport.views.get_metadata',
name='get_metadata'),
url(r'_get_options',
'websupport.views.get_options',
name='get_options'),
url(r'(?P<file>.*)',
'websupport.views.serve_file',
name='serve_file'),
)

View File

@ -0,0 +1,59 @@
import json
from .backend import DjangoStorage
from django.http import HttpResponse
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.views.decorators.csrf import csrf_exempt
from sphinx.websupport import WebSupport
storage = DjangoStorage()
support = WebSupport(
srcdir='/Users/eric/projects/readthedocs.org/docs',
builddir='/Users/eric/projects/readthedocs.org/docs/_build/websupport',
datadir='/Users/eric/projects/readthedocs.org/docs/_build/websupport/data',
storage=storage,
docroot='websupport',
)
def jsonify(obj):
return HttpResponse(json.dumps(obj), mimetype='text/javascript')
def build(request):
support.build()
def serve_file(request, file):
document = support.get_document(file)
return render_to_response('doc.html',
{'document': document},
context_instance=RequestContext(request))
@csrf_exempt
def add_comment(request):
parent_id = request.POST.get('parent', '')
node_id = request.POST.get('node', '')
text = request.POST.get('text', '')
proposal = request.POST.get('proposal', '')
username = None
comment = support.add_comment(text=text, node_id=node_id,
parent_id=parent_id,
username=username, proposal=proposal)
return jsonify(comment)
def get_comments(request):
username = None
node_id = request.GET.get('node', '')
data = support.get_data(node_id, username=username)
return jsonify(data)
def get_metadata(request):
document = request.GET.get('document', '')
return jsonify(storage.get_metadata(docname=document))
def get_options(request):
document = request.GET.get('document', '')
return jsonify(support.base_comment_opts)