Merge branch 'pr-83', closes #83

This commit is contained in:
Martin Zimmermann 2014-05-17 11:07:51 +02:00
commit fb182ae93f
13 changed files with 281 additions and 34 deletions

View File

@ -10,6 +10,9 @@ preferably in the script tag which embeds the JS:
data-isso-css="true"
data-isso-lang="ru"
data-isso-reply-to-self="false"
data-isso-max-comments-top="10"
data-isso-max-comments-nested="5"
data-isso-reveal-on-click="5"
data-avatar-bg="#f0f0f0"
data-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."
src="/prefix/js/embed.js"></script>
@ -53,6 +56,19 @@ data-isso-reply-to-self
Set to `true` when spam guard is configured with `reply-to-self = true`.
data-isso-max-comments-top and data-isso-max-comments-nested
------------------------------------------------------------
Number of top level (or nested) comments to show by default. If some
comments are not shown, an "X Hidden" link is shown.
Set to `"inf"` to show all, or `"0"` to hide all.
data-isso-reveal-on-click
-------------------------
Number of comments to reveal on clicking the "X Hidden" link.
data-isso-avatar-bg
-------------------

View File

@ -97,15 +97,31 @@ class Comments:
return None
def fetch(self, uri, mode=5):
def fetch(self, uri, mode=5, after=0, parent='any', order_by='id', limit=None):
"""
Return comments for :param:`uri` with :param:`mode`.
"""
rv = self.db.execute([
'SELECT comments.* FROM comments INNER JOIN threads ON',
' threads.uri=? AND comments.tid=threads.id AND (? | comments.mode) = ?'
'ORDER BY id ASC;'], (uri, mode, mode)).fetchall()
sql = [ 'SELECT comments.* FROM comments INNER JOIN threads ON',
' threads.uri=? AND comments.tid=threads.id AND (? | comments.mode) = ?',
' AND comments.created>?']
sql_args = [uri, mode, mode, after]
if parent != 'any':
if parent is None:
sql.append('AND comments.parent IS NULL')
else:
sql.append('AND comments.parent=?')
sql_args.append(parent)
sql.append('ORDER BY ? ASC')
sql_args.append(order_by)
if limit:
sql.append('LIMIT ?')
sql_args.append(limit)
rv = self.db.execute(sql, sql_args).fetchall()
for item in rv:
yield dict(zip(Comments.fields, item))
@ -181,6 +197,17 @@ class Comments:
return {'likes': likes + 1, 'dislikes': dislikes}
return {'likes': likes, 'dislikes': dislikes + 1}
def reply_count(self, url, after=0):
"""
Return comment count for main thread and all reply threads for one url.
"""
sql = [ 'SELECT comments.parent,count(*) FROM comments INNER JOIN threads ON',
' threads.uri=? AND comments.tid=threads.id',
' AND comments.mode = 1 AND comments.created>? GROUP BY comments.parent;']
return dict(self.db.execute(sql, [url, after]).fetchall())
def count(self, *urls):
"""
Return comment count for one ore more urls..

View File

@ -78,7 +78,8 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) {
var qs = function(params) {
var rv = "";
for (var key in params) {
if (params.hasOwnProperty(key) && params[key]) {
if (params.hasOwnProperty(key) &&
params[key] !== null && typeof(params[key]) !== "undefined") {
rv += key + "=" + encodeURIComponent(params[key]) + "&";
}
}
@ -128,13 +129,27 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) {
return deferred.promise;
};
var fetch = function(tid) {
var fetch = function(tid, limit, nested_limit, parent, lastcreated) {
if (typeof(limit) === 'undefined') { limit = "inf"; }
if (typeof(nested_limit) === 'undefined') { nested_limit = "inf"; }
if (typeof(parent) === 'undefined') { parent = null; }
var query_dict = {uri: tid || location, after: lastcreated, parent: parent};
if(limit !== "inf") {
query_dict['limit'] = limit;
}
if(nested_limit !== "inf"){
query_dict['nested_limit'] = nested_limit;
}
var deferred = Q.defer();
curl("GET", endpoint + "/?" + qs({uri: tid || location}), null, function(rv) {
curl("GET", endpoint + "/?" +
qs(query_dict), null, function(rv) {
if (rv.status === 200) {
deferred.resolve(JSON.parse(rv.body));
} else if (rv.status === 404) {
deferred.resolve([]);
deferred.resolve({total_replies: 0});
} else {
deferred.reject(rv.body);
}

View File

@ -5,6 +5,9 @@ define(function() {
"css": true,
"lang": (navigator.language || navigator.userLanguage).split("-")[0],
"reply-to-self": false,
"max-comments-top": 10,
"max-comments-nested": 5,
"reveal-on-click": 5,
"avatar-bg": "#f0f0f0",
"avatar-fg": ["#9abf88", "#5698c4", "#e279a3", "#9163b6",
"#be5168", "#f19670", "#e4bf80", "#447c69"].join(" ")

View File

@ -15,6 +15,7 @@ define({
"comment-deleted": "Kommentar gelöscht.",
"comment-queued": "Kommentar muss noch freigeschaltet werden.",
"comment-anonymous": "Anonym",
"comment-hidden": "{{ hidden_replies }} versteckt",
"date-now": "eben jetzt",
"date-minute": "vor einer Minute\nvor {{ n }} Minuten",
"date-hour": "vor einer Stunde\nvor {{ n }} Stunden",

View File

@ -17,6 +17,7 @@ define({
"comment-deleted": "Comment deleted.",
"comment-queued": "Comment in queue for moderation.",
"comment-anonymous": "Anonymous",
"comment-hidden": "{{ hidden_replies }} Hidden",
"date-now": "right now",
"date-minute": "a minute ago\n{{ n }} minutes ago",

View File

@ -15,6 +15,7 @@ define({
"comment-deleted": "Commentaire supprimé.",
"comment-queued": "Commentaire en attente de modération.",
"comment-anonymous": "Anonyme",
"comment-hidden": "1 caché\n{{ hidden_replies }} cachés",
"date-now": "À l'instant'",
"date-minute": "Il y a une minute \n{{ n }} minutes",
"date-hour": "Il y a une heure\n{{ n }} heures ",

View File

@ -84,8 +84,54 @@ define(["app/text/html", "app/dom", "app/utils", "app/config", "app/api", "app/m
return el;
};
var insert = function(comment, scrollIntoView) {
var insert_loader = function(commentWrapper, lastcreated) {
var entrypoint;
if (commentWrapper.id === null) {
entrypoint = $("#isso-root");
commentWrapper.name = 'null';
} else {
entrypoint = $("#isso-" + commentWrapper.id + " > .text-wrapper > .isso-follow-up");
commentWrapper.name = commentWrapper.id;
}
var el = $.htmlify(Mark.up(templates["comment_loader"], commentWrapper));
entrypoint.append(el);
$("a.load_hidden", el).toggle("click",
function() {
el.remove();
api.fetch($("#isso-thread").getAttribute("data-isso-id"),
config["reveal-on-click"], config["max-comments-nested"],
commentWrapper.id,
lastcreated).then(
function(rv) {
if (rv.total_replies === 0) {
return;
}
var lastcreated = 0;
rv.replies.forEach(function(commentObject) {
insert(commentObject, false);
if(commentObject.created > lastcreated) {
lastcreated = commentObject.created;
}
});
if(rv.hidden_replies > 0) {
insert_loader(rv, lastcreated);
}
if (window.location.hash.length > 0) {
$(window.location.hash).scrollIntoView();
}
},
function(err) {
console.log(err);
});
});
};
var insert = function(comment, scrollIntoView) {
var el = $.htmlify(Mark.up(templates["comment"], comment));
// update datetime every 60 seconds
@ -126,13 +172,13 @@ define(["app/text/html", "app/dom", "app/utils", "app/config", "app/api", "app/m
$("a.reply", footer).textContent = msgs["comment-close"];
},
function() {
form.remove()
form.remove();
$("a.reply", footer).textContent = msgs["comment-reply"];
}
);
// update vote counter, but hide if votes sum to 0
var votes = function(value) {
var votes = function(value) {
var span = $("span.votes", footer);
if (span === null && value !== 0) {
footer.prepend($.new("span.votes", value));
@ -261,10 +307,27 @@ define(["app/text/html", "app/dom", "app/utils", "app/config", "app/api", "app/m
if (! config["reply-to-self"] && utils.cookie("isso-" + comment.id)) {
show($("a.reply", footer).detach());
}
if(comment.hasOwnProperty('replies')) {
var lastcreated = 0;
comment.replies.forEach(function(replyObject) {
insert(replyObject, false);
if(replyObject.created > lastcreated) {
lastcreated = replyObject.created;
}
});
if(comment.hidden_replies > 0) {
insert_loader(comment, lastcreated);
}
}
};
return {
insert: insert,
insert_loader: insert_loader,
Postbox: Postbox
};
});

View File

@ -0,0 +1,3 @@
<div class="isso-comment-loader" id="isso-loader-{{ name | blank }}">
<a class="load_hidden" href="#">{{ i18n-comment-hidden | pluralize : `hidden_replies` }}</a>
</div>

View File

@ -1,6 +1,7 @@
define(["text!./postbox.html", "text!./comment.html"], function (postbox, comment) {
define(["text!./postbox.html", "text!./comment.html", "text!./comment-loader.html"], function (postbox, comment, comment_loader) {
return {
postbox: postbox,
comment: comment
comment: comment,
comment_loader: comment_loader
};
});

View File

@ -26,16 +26,28 @@ require(["app/lib/ready", "app/config", "app/api", "app/isso", "app/count", "app
$("#isso-thread").append(new isso.Postbox(null));
$("#isso-thread").append('<div id="isso-root"></div>');
api.fetch($("#isso-thread").getAttribute("data-isso-id")).then(
api.fetch($("#isso-thread").getAttribute("data-isso-id"),
config["max-comments-top"],
config["max-comments-nested"]).then(
function(rv) {
if (! rv.length) {
if (rv.total_replies === 0) {
$("#isso-thread > h4").textContent = Mark.up("{{ i18n-no-comments }}");
return;
}
$("#isso-thread > h4").textContent = Mark.up("{{ i18n-num-comments | pluralize : `n` }}", {n: rv.length});
for (var i=0; i < rv.length; i++) {
isso.insert(rv[i], false);
var lastcreated = 0;
var total_count = rv.total_replies;
rv.replies.forEach(function(commentObject) {
isso.insert(commentObject, false);
if(commentObject.created > lastcreated) {
lastcreated = commentObject.created;
}
total_count = total_count + commentObject.total_replies;
});
$("#isso-thread > h4").textContent = Mark.up("{{ i18n-num-comments | pluralize : `n` }}", {n: total_count});
if(rv.hidden_replies > 0) {
isso.insert_loader(rv, lastcreated);
}
if (window.location.hash.length > 0) {

View File

@ -103,7 +103,7 @@ class TestComments(unittest.TestCase):
self.assertEqual(r.status_code, 200)
rv = loads(r.data)
self.assertEqual(len(rv), 20)
self.assertEqual(len(rv['replies']), 20)
def testCreateInvalidParent(self):
@ -138,6 +138,40 @@ class TestComments(unittest.TestCase):
self.assertEqual(self.get('/?uri=%2Fpath%2Fspam%2F&id=123').status_code, 404)
self.assertEqual(self.get('/?uri=?uri=%foo%2F').status_code, 404)
def testGetLimited(self):
for i in range(20):
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
r = self.get('/?uri=test&limit=10')
self.assertEqual(r.status_code, 200)
rv = loads(r.data)
self.assertEqual(len(rv['replies']), 10)
def testGetNested(self):
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
self.post('/new?uri=test', data=json.dumps({'text': '...', 'parent': 1}))
r = self.get('/?uri=test&parent=1')
self.assertEqual(r.status_code, 200)
rv = loads(r.data)
self.assertEqual(len(rv['replies']), 1)
def testGetLimitedNested(self):
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
for i in range(20):
self.post('/new?uri=test', data=json.dumps({'text': '...', 'parent': 1}))
r = self.get('/?uri=test&parent=1&limit=10')
self.assertEqual(r.status_code, 200)
rv = loads(r.data)
self.assertEqual(len(rv['replies']), 10)
def testUpdate(self):
self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'Lorem ipsum ...'}))

View File

@ -320,11 +320,81 @@ class API(object):
@requires(str, 'uri')
def fetch(self, environ, request, uri):
rv = list(self.comments.fetch(uri))
if not rv:
args = {
'uri': uri,
'after': request.args.get('after', 0)
}
try:
args['limit'] = int(request.args.get('limit'))
except TypeError:
args['limit'] = None
except ValueError:
return BadRequest("limit should be integer")
if request.args.get('parent') is not None:
try:
args['parent'] = int(request.args.get('parent'))
root_id = args['parent']
except ValueError:
return BadRequest("parent should be integer")
else:
args['parent'] = None
root_id = None
plain = request.args.get('plain', '0') == '0'
reply_counts = self.comments.reply_count(uri, args['after'])
if args['limit'] == 0:
root_list = []
else:
root_list = list(self.comments.fetch(**args))
if not root_list:
raise NotFound
for item in rv:
if root_id not in reply_counts:
reply_counts[root_id] = 0
try:
nested_limit = int(request.args.get('nested_limit'))
except TypeError:
nested_limit = None
except ValueError:
return BadRequest("nested_limit should be integer")
rv = {
'id' : root_id,
'total_replies' : reply_counts[root_id],
'hidden_replies' : reply_counts[root_id] - len(root_list),
'replies' : self._process_fetched_list(root_list, plain)
}
# We are only checking for one level deep comments
if root_id is None:
for comment in rv['replies']:
if comment['id'] in reply_counts:
comment['total_replies'] = reply_counts[comment['id']]
if nested_limit is not None:
if nested_limit > 0:
args['parent'] = comment['id']
args['limit'] = nested_limit
replies = list(self.comments.fetch(**args))
else:
replies = []
else:
args['parent'] = comment['id']
replies = list(self.comments.fetch(**args))
else:
comment['total_replies'] = 0
replies = []
comment['hidden_replies'] = comment['total_replies'] - len(replies)
comment['replies'] = self._process_fetched_list(replies, plain)
return JSON(rv, 200)
def _process_fetched_list(self, fetched_list, plain=False):
for item in fetched_list:
key = item['email'] or item['remote_addr']
val = self.cache.get('hash', key.encode('utf-8'))
@ -338,11 +408,11 @@ class API(object):
for key in set(item.keys()) - API.FIELDS:
item.pop(key)
if request.args.get('plain', '0') == '0':
for item in rv:
if plain:
for item in fetched_list:
item['text'] = self.isso.render(item['text'])
return JSON(rv, 200)
return fetched_list
@xhr
def like(self, environ, request, id):