Skip to content

Commit

Permalink
Add sorting option for comments
Browse files Browse the repository at this point in the history
Add sorting and pagination support for comments, including new API query parameters for sort order and offset. Adjust comment fetching logic accordingly.

Closes #4
  • Loading branch information
pkvach committed Apr 19, 2024
1 parent 29236ff commit d2ed180
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 40 deletions.
4 changes: 2 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ New Features
- Make <code class="language-$lang"> for syntax highlighting (`#998`_, pkvach)
- Add search for comments by URL in the admin interface (`#1000`_, pkvach)
- Add CSS variables for better organization and flexibility (`#1001`_, pkvach)
- Add sorting option for comments (`#1005`_, pkvach)

.. _#966: https://github.com/posativ/isso/pull/966
.. _#998: https://github.com/isso-comments/isso/pull/998
.. _#1000: https://github.com/isso-comments/isso/pull/1000
.. _#966: https://github.com/posativ/isso/pull/966
.. _#998: https://github.com/isso-comments/isso/pull/998
.. _#1001: https://github.com/isso-comments/isso/pull/1001
.. _#1005: https://github.com/isso-comments/isso/pull/1005

Breaking Changes
^^^^^^^^^^^^^^^^
Expand Down
14 changes: 14 additions & 0 deletions docs/docs/reference/client-config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ preferably in the script tag which embeds the JS:
data-isso-max-comments-top="10"
data-isso-max-comments-nested="5"
data-isso-reveal-on-click="5"
data-isso-sorting="newest"
data-isso-avatar="true"
data-isso-avatar-bg="#f0f0f0"
data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."
Expand Down Expand Up @@ -255,6 +256,19 @@ data-isso-reply-notifications-default-enabled
.. versionadded:: 0.13


.. _data-isso-sorting:

data-isso-sorting
A thread sorting method that are applied in the specified order.

Possible sorting methods:

- `newest`: Bring newest comments to the top
- `oldest`: Bring oldest comments to the top
- `upvotes`: Bring most liked comments to the top

Default sorting is `oldest`.

Deprecated Client Settings
--------------------------

Expand Down
15 changes: 9 additions & 6 deletions isso/db/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def fetchall(self, mode=5, after=0, parent='any', order_by='id',
yield dict(zip(fields_comments + fields_threads, item))

def fetch(self, uri, mode=5, after=0, parent='any',
order_by='id', asc=1, limit=None):
order_by='id', asc=1, limit=None, offset=0):
"""
Return comments for :param:`uri` with :param:`mode`.
"""
Expand All @@ -246,7 +246,11 @@ def fetch(self, uri, mode=5, after=0, parent='any',
if not asc:
sql.append(' DESC')

if limit:
if offset and limit:
sql.append('LIMIT ?,?')
sql_args.append(offset)
sql_args.append(limit)
elif limit:
sql.append('LIMIT ?')
sql_args.append(limit)

Expand Down Expand Up @@ -333,19 +337,18 @@ def vote(self, upvote, id, remote_addr):
return {'likes': likes + 1, 'dislikes': dislikes}
return {'likes': likes, 'dislikes': dislikes + 1}

def reply_count(self, url, mode=5, after=0):
def reply_count(self, url, mode=5):
"""
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 = ?) AND',
' comments.created > ?',
' (? | comments.mode = ?)',
'GROUP BY comments.parent']

return dict(self.db.execute(sql, [url, mode, mode, after]).fetchall())
return dict(self.db.execute(sql, [url, mode, mode]).fetchall())

def count(self, *urls):
"""
Expand Down
6 changes: 4 additions & 2 deletions isso/js/app/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,14 @@ var view = function(id, plain) {
return deferred.promise;
};

var fetch = function(tid, limit, nested_limit, parent, lastcreated) {
var fetch = function(tid, limit, nested_limit, parent, sort, offset) {
if (typeof(limit) === 'undefined') { limit = "inf"; }
if (typeof(offset) === 'undefined') { offset = 0; }
if (typeof(sort) === 'undefined') { sort = ""; }
if (typeof(nested_limit) === 'undefined') { nested_limit = "inf"; }
if (typeof(parent) === 'undefined') { parent = null; }

var query_dict = {uri: tid || location(), after: lastcreated, parent: parent};
var query_dict = {uri: tid || location(), sort: sort, parent: parent, offset: offset};

if(limit !== "inf") {
query_dict['limit'] = limit;
Expand Down
1 change: 1 addition & 0 deletions isso/js/app/default_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var default_config = {
"max-comments-top": "inf",
"max-comments-nested": 5,
"reveal-on-click": 5,
"sorting": "oldest",
"gravatar": false,
"avatar": true,
"avatar-bg": "#f0f0f0",
Expand Down
33 changes: 12 additions & 21 deletions isso/js/app/isso.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ var Postbox = function(parent) {
notification: $("[name=notification]", el).checked() ? 1 : 0,
}).then(function(comment) {
$(".isso-textarea", el).value = "";
insert(comment, true);
insert(comment, true, 0);

if (parent !== null) {
el.onsuccess();
Expand All @@ -125,7 +125,7 @@ var Postbox = function(parent) {
return el;
};

var insert_loader = function(comment, lastcreated) {
var insert_loader = function(comment, offset) {
var entrypoint;
if (comment.id === null) {
entrypoint = $("#isso-root");
Expand All @@ -143,22 +143,19 @@ var insert_loader = function(comment, lastcreated) {
api.fetch($("#isso-thread").getAttribute("data-isso-id"),
config["reveal-on-click"], config["max-comments-nested"],
comment.id,
lastcreated).then(
config["sorting"],
offset).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;
}
insert(commentObject, false, 0);
});

if(rv.hidden_replies > 0) {
insert_loader(rv, lastcreated);
insert_loader(rv, offset + rv.replies.length);
}
},
function(err) {
Expand All @@ -167,7 +164,7 @@ var insert_loader = function(comment, lastcreated) {
});
};

var insert = function(comment, scrollIntoView) {
var insert = function(comment, scrollIntoView, offset) {
var el = $.htmlify(template.render("comment", {"comment": comment}));

// update datetime every 60 seconds
Expand Down Expand Up @@ -381,19 +378,13 @@ var insert = function(comment, scrollIntoView) {
show($("a.isso-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.hasOwnProperty('replies')) {
comment.replies.forEach(function (replyObject) {
insert(replyObject, false, offset + 1);
});
if(comment.hidden_replies > 0) {
insert_loader(comment, lastcreated);
if (comment.hidden_replies > 0) {
insert_loader(comment, offset + comment.replies.length);
}

}

};
Expand Down
13 changes: 6 additions & 7 deletions isso/js/embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,27 +124,26 @@ function fetchComments() {

api.fetch(isso_thread.getAttribute("data-isso-id") || location.pathname,
config["max-comments-top"],
config["max-comments-nested"]).then(
config["max-comments-nested"],
null,
config["sorting"],
0).then(
function (rv) {

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

var lastcreated = 0;
var count = rv.total_replies;
rv.replies.forEach(function(comment) {
isso.insert(comment, false);
if (comment.created > lastcreated) {
lastcreated = comment.created;
}
isso.insert(comment, false, 0);
count = count + comment.total_replies;
});
heading.textContent = i18n.pluralize("num-comments", count);

if (rv.hidden_replies > 0) {
isso.insert_loader(rv, lastcreated);
isso.insert_loader(rv, rv.replies.length);
}

if (window.location.hash.length > 0 &&
Expand Down
128 changes: 128 additions & 0 deletions isso/tests/test_comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,134 @@ def testGetLimitedNested(self):
rv = loads(r.data)
self.assertEqual(len(rv['replies']), 10)

def testGetWithOffset(self):
for i in range(5):
self.post('/new?uri=test', data=json.dumps({'text': '...'}))

r = self.get('/?uri=test&limit=3&offset=2')
self.assertEqual(r.status_code, 200)

rv = loads(r.data)
self.assertEqual(
[comment['id'] for comment in rv['replies']],
[3, 4, 5]
)

def testGetWithOffsetIgnoredWithoutLimit(self):
for i in range(5):
self.post('/new?uri=test', data=json.dumps({'text': '...'}))

r = self.get('/?uri=test&offset=2')
self.assertEqual(r.status_code, 200)

rv = loads(r.data)
self.assertEqual(
[comment['id'] for comment in rv['replies']],
[1, 2, 3, 4, 5]
)

def testGetNestedWithOffset(self):

self.post('/new?uri=test', data=json.dumps({'text': '...'}))
for i in range(5):
self.post('/new?uri=test',
data=json.dumps({'text': '...', 'parent': 1}))

r = self.get('/?uri=test&parent=1&limit=3&offset=2')
self.assertEqual(r.status_code, 200)

rv = loads(r.data)
self.assertEqual(
[comment['id'] for comment in rv['replies']],
[4, 5, 6]
)

def testGetSortedByOldest(self):
for i in range(5):
self.post('/new?uri=test', data=json.dumps({'text': '...'}))

r = self.get('/?uri=test&sort=oldest')
self.assertEqual(r.status_code, 200)

rv = loads(r.data)
# assert order of comments is oldest first
self.assertEqual(
[comment['id'] for comment in rv['replies']],
[1, 2, 3, 4, 5]
)

def testGetSortedByNewest(self):
for i in range(5):
self.post('/new?uri=test', data=json.dumps({'text': '...'}))

r = self.get('/?uri=test&sort=newest')
self.assertEqual(r.status_code, 200)

rv = loads(r.data)
# assert order of comments is newest first
self.assertEqual(
[comment['id'] for comment in rv['replies']],
[5, 4, 3, 2, 1]
)

def testGetSortedByUpvotes(self):
for i in range(5):
self.post('/new?uri=test', data=json.dumps({'text': '...'}))

# update the likes for some comments
self.app.db.execute(
'UPDATE comments SET likes = id WHERE id IN (2, 4)'
)

r = self.get('/?uri=test&sort=upvotes')
self.assertEqual(r.status_code, 200)

rv = loads(r.data)
# assert order of comments is by upvotes
self.assertEqual(
[comment['id'] for comment in rv['replies']],
[4, 2, 1, 3, 5]
)

def testGetSortedByNewestWithNested(self):
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
for i in range(5):
self.post('/new?uri=test',
data=json.dumps({'text': '...', 'parent': 1}))

r = self.get('/?uri=test&sort=newest')
self.assertEqual(r.status_code, 200)

rv = loads(r.data)
self.assertEqual(len(rv['replies']), 1)
# assert order of nested comments is newest first
self.assertEqual(
[comment['id'] for comment in rv['replies'][0]['replies']],
[6, 5, 4, 3, 2]
)

def testGetSortedByUpvotesWithNested(self):
self.post('/new?uri=test', data=json.dumps({'text': '...'}))
for i in range(5):
self.post('/new?uri=test',
data=json.dumps({'text': '...', 'parent': 1}))

# update the likes for some comments
self.app.db.execute(
'UPDATE comments SET likes = id WHERE id IN (3, 6)'
)

r = self.get('/?uri=test&sort=upvotes')
self.assertEqual(r.status_code, 200)

rv = loads(r.data)
self.assertEqual(len(rv['replies']), 1)
# assert order of nested comments is newest first
self.assertEqual(
[comment['id'] for comment in rv['replies'][0]['replies']],
[6, 3, 2, 4, 5]
)

def testUpdate(self):

self.post('/new?uri=%2Fpath%2F',
Expand Down
Loading

0 comments on commit d2ed180

Please sign in to comment.