Skip to content

Commit ec0283b

Browse files
committed
Add sorting option for comments
Add sorting and pagination support for comments, including new API query parameters for sort order and offset. Adjust comment fetching logic accordingly. Closes #4
1 parent 6dffaa7 commit ec0283b

9 files changed

Lines changed: 208 additions & 38 deletions

File tree

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ New Features
99

1010
- Add Catalan localisation (`#966`_, welpo)
1111
- Make <code class="language-$lang"> for syntax highlighting (`#998`_, pkvach)
12+
- Add sorting option for comments (`#1005`_, pkvach)
1213

1314
.. _#966: https://github.com/posativ/isso/pull/966
1415
.. _#998: https://github.com/isso-comments/isso/pull/998
16+
.. _#1005: https://github.com/isso-comments/isso/pull/1005
1517

1618
Breaking Changes
1719
^^^^^^^^^^^^^^^^

docs/docs/reference/client-config.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ preferably in the script tag which embeds the JS:
1313
data-isso-max-comments-top="10"
1414
data-isso-max-comments-nested="5"
1515
data-isso-reveal-on-click="5"
16+
data-isso-sorting="newest"
1617
data-isso-avatar="true"
1718
data-isso-avatar-bg="#f0f0f0"
1819
data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."
@@ -255,6 +256,19 @@ data-isso-reply-notifications-default-enabled
255256
.. versionadded:: 0.13
256257

257258

259+
.. _data-isso-sorting:
260+
261+
data-isso-sorting
262+
A thread sorting method that are applied in the specified order.
263+
264+
Possible sorting methods:
265+
266+
- `newest`: Bring newest comments to the top
267+
- `oldest`: Bring oldest comments to the top
268+
- `upvotes`: Bring most liked comments to the top
269+
270+
Default sorting is `oldest`.
271+
258272
Deprecated Client Settings
259273
--------------------------
260274

isso/db/comments.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def fetchall(self, mode=5, after=0, parent='any', order_by='id',
215215
yield dict(zip(fields_comments + fields_threads, item))
216216

217217
def fetch(self, uri, mode=5, after=0, parent='any',
218-
order_by='id', asc=1, limit=None):
218+
order_by='id', asc=1, limit=None, offset=0):
219219
"""
220220
Return comments for :param:`uri` with :param:`mode`.
221221
"""
@@ -240,7 +240,11 @@ def fetch(self, uri, mode=5, after=0, parent='any',
240240
if not asc:
241241
sql.append(' DESC')
242242

243-
if limit:
243+
if offset and limit:
244+
sql.append('LIMIT ?,?')
245+
sql_args.append(offset)
246+
sql_args.append(limit)
247+
elif limit:
244248
sql.append('LIMIT ?')
245249
sql_args.append(limit)
246250

@@ -327,19 +331,18 @@ def vote(self, upvote, id, remote_addr):
327331
return {'likes': likes + 1, 'dislikes': dislikes}
328332
return {'likes': likes, 'dislikes': dislikes + 1}
329333

330-
def reply_count(self, url, mode=5, after=0):
334+
def reply_count(self, url, mode=5):
331335
"""
332336
Return comment count for main thread and all reply threads for one url.
333337
"""
334338

335339
sql = ['SELECT comments.parent,count(*)',
336340
'FROM comments INNER JOIN threads ON',
337341
' threads.uri=? AND comments.tid=threads.id AND',
338-
' (? | comments.mode = ?) AND',
339-
' comments.created > ?',
342+
' (? | comments.mode = ?)',
340343
'GROUP BY comments.parent']
341344

342-
return dict(self.db.execute(sql, [url, mode, mode, after]).fetchall())
345+
return dict(self.db.execute(sql, [url, mode, mode]).fetchall())
343346

344347
def count(self, *urls):
345348
"""

isso/js/app/api.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,14 @@ var view = function(id, plain) {
138138
return deferred.promise;
139139
};
140140

141-
var fetch = function(tid, limit, nested_limit, parent, lastcreated) {
141+
var fetch = function(tid, limit, nested_limit, parent, sort, offset) {
142142
if (typeof(limit) === 'undefined') { limit = "inf"; }
143+
if (typeof(offset) === 'undefined') { offset = 0; }
144+
if (typeof(sort) === 'undefined') { sort = ""; }
143145
if (typeof(nested_limit) === 'undefined') { nested_limit = "inf"; }
144146
if (typeof(parent) === 'undefined') { parent = null; }
145147

146-
var query_dict = {uri: tid || location(), after: lastcreated, parent: parent};
148+
var query_dict = {uri: tid || location(), sort: sort, parent: parent, offset: offset};
147149

148150
if(limit !== "inf") {
149151
query_dict['limit'] = limit;

isso/js/app/default_config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var default_config = {
1313
"max-comments-top": "inf",
1414
"max-comments-nested": 5,
1515
"reveal-on-click": 5,
16+
"sorting": "oldest",
1617
"gravatar": false,
1718
"avatar": true,
1819
"avatar-bg": "#f0f0f0",

isso/js/app/isso.js

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ var Postbox = function(parent) {
114114
notification: $("[name=notification]", el).checked() ? 1 : 0,
115115
}).then(function(comment) {
116116
$(".isso-textarea", el).value = "";
117-
insert(comment, true);
117+
insert(comment, true, 0);
118118

119119
if (parent !== null) {
120120
el.onsuccess();
@@ -125,7 +125,7 @@ var Postbox = function(parent) {
125125
return el;
126126
};
127127

128-
var insert_loader = function(comment, lastcreated) {
128+
var insert_loader = function(comment, offset) {
129129
var entrypoint;
130130
if (comment.id === null) {
131131
entrypoint = $("#isso-root");
@@ -143,22 +143,19 @@ var insert_loader = function(comment, lastcreated) {
143143
api.fetch($("#isso-thread").getAttribute("data-isso-id"),
144144
config["reveal-on-click"], config["max-comments-nested"],
145145
comment.id,
146-
lastcreated).then(
146+
config["sorting"],
147+
offset).then(
147148
function(rv) {
148149
if (rv.total_replies === 0) {
149150
return;
150151
}
151152

152-
var lastcreated = 0;
153153
rv.replies.forEach(function(commentObject) {
154-
insert(commentObject, false);
155-
if(commentObject.created > lastcreated) {
156-
lastcreated = commentObject.created;
157-
}
154+
insert(commentObject, false, 0);
158155
});
159156

160157
if(rv.hidden_replies > 0) {
161-
insert_loader(rv, lastcreated);
158+
insert_loader(rv, offset + rv.replies.length);
162159
}
163160
},
164161
function(err) {
@@ -167,7 +164,7 @@ var insert_loader = function(comment, lastcreated) {
167164
});
168165
};
169166

170-
var insert = function(comment, scrollIntoView) {
167+
var insert = function(comment, scrollIntoView, offset) {
171168
var el = $.htmlify(template.render("comment", {"comment": comment}));
172169

173170
// update datetime every 60 seconds
@@ -381,19 +378,13 @@ var insert = function(comment, scrollIntoView) {
381378
show($("a.isso-reply", footer).detach());
382379
}
383380

384-
if(comment.hasOwnProperty('replies')) {
385-
var lastcreated = 0;
386-
comment.replies.forEach(function(replyObject) {
387-
insert(replyObject, false);
388-
if(replyObject.created > lastcreated) {
389-
lastcreated = replyObject.created;
390-
}
391-
381+
if (comment.hasOwnProperty('replies')) {
382+
comment.replies.forEach(function (replyObject) {
383+
insert(replyObject, false, offset + 1);
392384
});
393-
if(comment.hidden_replies > 0) {
394-
insert_loader(comment, lastcreated);
385+
if (comment.hidden_replies > 0) {
386+
insert_loader(comment, offset + comment.replies.length);
395387
}
396-
397388
}
398389

399390
};

isso/js/embed.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,27 +124,26 @@ function fetchComments() {
124124

125125
api.fetch(isso_thread.getAttribute("data-isso-id") || location.pathname,
126126
config["max-comments-top"],
127-
config["max-comments-nested"]).then(
127+
config["max-comments-nested"],
128+
null,
129+
config["sorting"],
130+
0).then(
128131
function (rv) {
129132

130133
if (rv.total_replies === 0) {
131134
heading.textContent = i18n.translate("no-comments");
132135
return;
133136
}
134137

135-
var lastcreated = 0;
136138
var count = rv.total_replies;
137139
rv.replies.forEach(function(comment) {
138-
isso.insert(comment, false);
139-
if (comment.created > lastcreated) {
140-
lastcreated = comment.created;
141-
}
140+
isso.insert(comment, false, 0);
142141
count = count + comment.total_replies;
143142
});
144143
heading.textContent = i18n.pluralize("num-comments", count);
145144

146145
if (rv.hidden_replies > 0) {
147-
isso.insert_loader(rv, lastcreated);
146+
isso.insert_loader(rv, rv.replies.length);
148147
}
149148

150149
if (window.location.hash.length > 0 &&

isso/tests/test_comments.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,134 @@ def testGetLimitedNested(self):
240240
rv = loads(r.data)
241241
self.assertEqual(len(rv['replies']), 10)
242242

243+
def testGetWithOffset(self):
244+
for i in range(5):
245+
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
246+
247+
r = self.get('/?uri=test&limit=3&offset=2')
248+
self.assertEqual(r.status_code, 200)
249+
250+
rv = loads(r.data)
251+
self.assertEqual(
252+
[comment['id'] for comment in rv['replies']],
253+
[3, 4, 5]
254+
)
255+
256+
def testGetWithOffsetIgnoredWithoutLimit(self):
257+
for i in range(5):
258+
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
259+
260+
r = self.get('/?uri=test&offset=2')
261+
self.assertEqual(r.status_code, 200)
262+
263+
rv = loads(r.data)
264+
self.assertEqual(
265+
[comment['id'] for comment in rv['replies']],
266+
[1, 2, 3, 4, 5]
267+
)
268+
269+
def testGetNestedWithOffset(self):
270+
271+
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
272+
for i in range(5):
273+
self.post('/new?uri=test',
274+
data=json.dumps({'text': '...', 'parent': 1}))
275+
276+
r = self.get('/?uri=test&parent=1&limit=3&offset=2')
277+
self.assertEqual(r.status_code, 200)
278+
279+
rv = loads(r.data)
280+
self.assertEqual(
281+
[comment['id'] for comment in rv['replies']],
282+
[4, 5, 6]
283+
)
284+
285+
def testGetSortedByOldest(self):
286+
for i in range(5):
287+
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
288+
289+
r = self.get('/?uri=test&sort=oldest')
290+
self.assertEqual(r.status_code, 200)
291+
292+
rv = loads(r.data)
293+
# assert order of comments is oldest first
294+
self.assertEqual(
295+
[comment['id'] for comment in rv['replies']],
296+
[1, 2, 3, 4, 5]
297+
)
298+
299+
def testGetSortedByNewest(self):
300+
for i in range(5):
301+
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
302+
303+
r = self.get('/?uri=test&sort=newest')
304+
self.assertEqual(r.status_code, 200)
305+
306+
rv = loads(r.data)
307+
# assert order of comments is newest first
308+
self.assertEqual(
309+
[comment['id'] for comment in rv['replies']],
310+
[5, 4, 3, 2, 1]
311+
)
312+
313+
def testGetSortedByUpvotes(self):
314+
for i in range(5):
315+
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
316+
317+
# update the likes for some comments
318+
self.app.db.execute(
319+
'UPDATE comments SET likes = id WHERE id IN (2, 4)'
320+
)
321+
322+
r = self.get('/?uri=test&sort=upvotes')
323+
self.assertEqual(r.status_code, 200)
324+
325+
rv = loads(r.data)
326+
# assert order of comments is by upvotes
327+
self.assertEqual(
328+
[comment['id'] for comment in rv['replies']],
329+
[4, 2, 1, 3, 5]
330+
)
331+
332+
def testGetSortedByNewestWithNested(self):
333+
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
334+
for i in range(5):
335+
self.post('/new?uri=test',
336+
data=json.dumps({'text': '...', 'parent': 1}))
337+
338+
r = self.get('/?uri=test&sort=newest')
339+
self.assertEqual(r.status_code, 200)
340+
341+
rv = loads(r.data)
342+
self.assertEqual(len(rv['replies']), 1)
343+
# assert order of nested comments is newest first
344+
self.assertEqual(
345+
[comment['id'] for comment in rv['replies'][0]['replies']],
346+
[6, 5, 4, 3, 2]
347+
)
348+
349+
def testGetSortedByUpvotesWithNested(self):
350+
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
351+
for i in range(5):
352+
self.post('/new?uri=test',
353+
data=json.dumps({'text': '...', 'parent': 1}))
354+
355+
# update the likes for some comments
356+
self.app.db.execute(
357+
'UPDATE comments SET likes = id WHERE id IN (3, 6)'
358+
)
359+
360+
r = self.get('/?uri=test&sort=upvotes')
361+
self.assertEqual(r.status_code, 200)
362+
363+
rv = loads(r.data)
364+
self.assertEqual(len(rv['replies']), 1)
365+
# assert order of nested comments is newest first
366+
self.assertEqual(
367+
[comment['id'] for comment in rv['replies'][0]['replies']],
368+
[6, 3, 2, 4, 5]
369+
)
370+
243371
def testUpdate(self):
244372

245373
self.post('/new?uri=%2Fpath%2F',

0 commit comments

Comments
 (0)