add POST request to get comment counts for multiple URLs

The old way via `GET /count?uri=...` still works, but is now deprecated
and might be removed in future releases.
The new way is much more efficient especially fore multiple listings.

The internal implemention is improvable though.
This commit is contained in:
Martin Zimmermann 2014-03-25 18:38:17 +01:00
parent 0d6d072281
commit 5ce48de94a
6 changed files with 66 additions and 14 deletions

View File

@ -8,6 +8,8 @@ if not PY2K:
map, zip, filter = map, zip, filter map, zip, filter = map, zip, filter
from functools import reduce from functools import reduce
iteritems = lambda dikt: iter(dikt.items())
text_type = str text_type = str
string_types = (str, ) string_types = (str, )
@ -18,6 +20,8 @@ else:
map, zip, filter = imap, izip, ifilter map, zip, filter = imap, izip, ifilter
reduce = reduce reduce = reduce
iteritems = lambda dikt: dikt.iteritems()
text_type = unicode text_type = unicode
string_types = (str, unicode) string_types = (str, unicode)

View File

@ -175,14 +175,18 @@ class Comments:
return {'likes': likes + 1, 'dislikes': dislikes} return {'likes': likes + 1, 'dislikes': dislikes}
return {'likes': likes, 'dislikes': dislikes + 1} return {'likes': likes, 'dislikes': dislikes + 1}
def count(self, uri): def count(self, *urls):
""" """
Return comment count for :param:`uri`. Return comment count for one ore more urls..
""" """
return self.db.execute([
'SELECT COUNT(comments.id) FROM comments INNER JOIN threads ON', threads = dict(self.db.execute([
' threads.uri=? AND comments.tid=threads.id AND comments.mode=1;'], 'SELECT threads.uri, COUNT(comments.id) FROM comments',
(uri, )).fetchone() 'LEFT OUTER JOIN threads ON threads.id = tid',
'GROUP BY threads.uri'
]).fetchall())
return [threads.get(url, 0) for url in urls]
def purge(self, delta): def purge(self, delta):
""" """

View File

@ -139,13 +139,11 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) {
return deferred.promise; return deferred.promise;
}; };
var count = function(tid) { var count = function(urls) {
var deferred = Q.defer(); var deferred = Q.defer();
curl("GET", endpoint + "/count?" + qs({uri: tid || location}), null, function(rv) { curl("POST", endpoint + "/count", JSON.stringify(urls), function(rv) {
if (rv.status === 200) { if (rv.status === 200) {
deferred.resolve(JSON.parse(rv.body)); deferred.resolve(JSON.parse(rv.body));
} else if (rv.status === 404) {
deferred.resolve(0);
} else { } else {
deferred.reject(rv.body); deferred.reject(rv.body);
} }

View File

@ -1,5 +1,8 @@
define(["app/api", "app/dom", "app/markup"], function(api, $, Mark) { define(["app/api", "app/dom", "app/markup"], function(api, $, Mark) {
return function() { return function() {
var objs = {};
$.each("a", function(el) { $.each("a", function(el) {
if (! el.href.match("#isso-thread$")) { if (! el.href.match("#isso-thread$")) {
return; return;
@ -9,9 +12,28 @@ define(["app/api", "app/dom", "app/markup"], function(api, $, Mark) {
el.href.match("^(.+)#isso-thread$")[1] el.href.match("^(.+)#isso-thread$")[1]
.replace(/^.*\/\/[^\/]+/, ''); .replace(/^.*\/\/[^\/]+/, '');
api.count(tid).then(function(rv) { if (tid in objs) {
el.textContent = Mark.up("{{ i18n-num-comments | pluralize : `n` }}", {n: rv}); objs[tid].push(el);
}); } else {
objs[tid] = [el];
}
});
var urls = Object.keys(objs);
api.count(urls).then(function(rv) {
for (var key in objs) {
if (objs.hasOwnProperty(key)) {
var index = urls.indexOf(key);
for (var i = 0; i < objs[key].length; i++) {
objs[key][i].textContent = Mark.up(
"{{ i18n-num-comments | pluralize : `n` }}",
{n: rv[index]});
}
}
}
}); });
}; };
}); });

View File

@ -16,7 +16,7 @@ from werkzeug.exceptions import BadRequest, Forbidden, NotFound
from isso.compat import text_type as str from isso.compat import text_type as str
from isso import utils, local from isso import utils, local
from isso.utils import http, parse, html, JSONResponse as JSON from isso.utils import http, parse, JSONResponse as JSON
from isso.utils.crypto import pbkdf2 from isso.utils.crypto import pbkdf2
from isso.views import requires from isso.views import requires
@ -61,6 +61,7 @@ class API(object):
('fetch', ('GET', '/')), ('fetch', ('GET', '/')),
('new', ('POST', '/new')), ('new', ('POST', '/new')),
('count', ('GET', '/count')), ('count', ('GET', '/count')),
('counts', ('POST', '/count')),
('view', ('GET', '/id/<int:id>')), ('view', ('GET', '/id/<int:id>')),
('edit', ('PUT', '/id/<int:id>')), ('edit', ('PUT', '/id/<int:id>')),
('delete', ('DELETE', '/id/<int:id>')), ('delete', ('DELETE', '/id/<int:id>')),
@ -352,6 +353,7 @@ class API(object):
nv = self.comments.vote(False, id, utils.anonymize(str(request.remote_addr))) nv = self.comments.vote(False, id, utils.anonymize(str(request.remote_addr)))
return JSON(nv, 200) return JSON(nv, 200)
# TODO: remove someday (replaced by :func:`counts`)
@requires(str, 'uri') @requires(str, 'uri')
def count(self, environ, request, uri): def count(self, environ, request, uri):
@ -362,5 +364,14 @@ class API(object):
return JSON(rv, 200) return JSON(rv, 200)
def counts(self, environ, request):
data = request.get_json()
if not isinstance(data, list) and not all(isinstance(x, str) for x in data):
raise BadRequest("JSON must be a list of URLs")
return JSON(self.comments.count(*data), 200)
def checkip(self, env, req): def checkip(self, env, req):
return Response(utils.anonymize(str(req.remote_addr)), 200) return Response(utils.anonymize(str(req.remote_addr)), 200)

View File

@ -22,6 +22,8 @@ from isso import Isso, core
from isso.utils import http from isso.utils import http
from isso.views import comments from isso.views import comments
from isso.compat import iteritems
from fixtures import curl, loads, FakeIP, JSONClient from fixtures import curl, loads, FakeIP, JSONClient
http.curl = curl http.curl = curl
@ -274,6 +276,17 @@ class TestComments(unittest.TestCase):
rv = self.get('/count?uri=%2Fpath%2F') rv = self.get('/count?uri=%2Fpath%2F')
self.assertEqual(rv.status_code, 404) self.assertEqual(rv.status_code, 404)
def testMultipleCounts(self):
expected = {'a': 1, 'b': 2, 'c': 0}
for uri, count in iteritems(expected):
for _ in range(count):
self.post('/new?uri=%s' % uri, data=json.dumps({"text": "..."}))
rv = self.post('/count', data=json.dumps(list(expected.keys())))
self.assertEqual(loads(rv.data), list(expected.values()))
def testModify(self): def testModify(self):
self.post('/new?uri=test', data=json.dumps({"text": "Tpyo"})) self.post('/new?uri=test', data=json.dumps({"text": "Tpyo"}))