From aa373f798ff22844692341c60c605bf627744cd7 Mon Sep 17 00:00:00 2001 From: Joshua Gleitze Date: Thu, 2 Jun 2016 22:01:49 +0200 Subject: [PATCH 01/78] + apidoc.json The apidoc.json file configures the ApiDoc tools. It generates AJAX API documentation out of comments in the source code. --- apidoc.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 apidoc.json diff --git a/apidoc.json b/apidoc.json new file mode 100644 index 0000000..760e429 --- /dev/null +++ b/apidoc.json @@ -0,0 +1,6 @@ +{ + "name": "isso", + "description": "a Disqus alternative", + "title": "isso API" +} + From 5ca5d680fa0eb9496bbab22e2331ad59c8658e6b Mon Sep 17 00:00:00 2001 From: Joshua Gleitze Date: Thu, 2 Jun 2016 22:02:06 +0200 Subject: [PATCH 02/78] apidoc for fetch --- isso/views/comments.py | 84 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/isso/views/comments.py b/isso/views/comments.py index 0272fa3..baa720a 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -347,6 +347,90 @@ class API(object): return Response("Yo", 200) + + """ + @api {get} / get comments + @apiGroup Thread + @apiDescription Queries the comments of a thread. + + @apiParam {string} uri + The URI of thread to get the comments from. + @apiParam {number} [parent] + Return only comments that are children of the comment with the provided ID. + @apiParam {number=0,1} [plain] + Iff set to `1`, the plain text entered by the user will be returned in the comments’ `text` attribute (instead of the rendered markdown). + @apiParam {number} [limit] + The maximum number of returned top-level comments. Omit for unlimited results. + @apiParam {number} [nested_limit] + The maximum number of returned nested comments per commint. Omit for unlimited results. + @apiParam {number} [after] + Includes only comments were added after the provided UNIX timestamp. + + @apiSuccess {number} total_replies + The number of replies if the `limit` parameter was not set. If `after` is set to `X`, this is the number of comments that were created after `X`. So setting `after` may change this value! + @apiSuccess {Object[]} replies + The list of comments. Each comment also has the `total_replies`, `replies`, `id` and `hidden_replies` properties to represent nested comments. + @apiSuccess {number} id + Id of the comment `replies` is the list of replies of. `null` for the list of toplevel comments. + @apiSuccess {number} hidden_replies + The number of comments that were ommited from the results because of the `limit` request parameter. Usually, this will be `total_replies` - `limit`. + + @apiExample {curl} Get 2 comments with 5 responses: + curl 'https://comments.example.com/?uri=/thread/&limit=2&nested_limit=5' + @apiSuccessExample Example reponse: + { + "total_replies": 14, + "replies": [ + { + "website": null, + "author": null, + "parent": null, + "created": 1464818460.732863, + "text": "<p>Hello, World!</p>", + "total_replies": 1, + "hidden_replies": 0, + "dislikes": 2, + "modified": null, + "mode": 1, + "replies": [ + { + "website": null, + "author": null, + "parent": 1, + "created": 1464818460.769638, + "text": "<p>Hi, now some Markdown: <em>Italic</em>, <strong>bold</strong>, <code>monospace</code>.</p>", + "dislikes": 0, + "modified": null, + "mode": 1, + "hash": "2af4e1a6c96a", + "id": 2, + "likes": 2 + } + ], + "hash": "1cb6cc0309a2", + "id": 1, + "likes": 2 + }, + { + "website": null, + "author": null, + "parent": null, + "created": 1464818460.80574, + "text": "<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusantium at commodi cum deserunt dolore, error fugiat harum incidunt, ipsa ipsum mollitia nam provident rerum sapiente suscipit tempora vitae? Est, qui?</p>", + "total_replies": 0, + "hidden_replies": 0, + "dislikes": 0, + "modified": null, + "mode": 1, + "replies": [], + "hash": "1cb6cc0309a2", + "id": 3, + "likes": 0 + }, + "id": null, + "hidden_replies": 12 + } + """ @requires(str, 'uri') def fetch(self, environ, request, uri): From b2d9c80b5f17fb86b797ff86da6acb9dd2805bd0 Mon Sep 17 00:00:00 2001 From: Joshua Gleitze Date: Fri, 3 Jun 2016 10:13:13 +0200 Subject: [PATCH 03/78] apidoc for "new comment" --- isso/views/comments.py | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/isso/views/comments.py b/isso/views/comments.py index baa720a..8cf0cab 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -60,6 +60,12 @@ def xhr(func): not forged (XHR is restricted by CORS separately). """ + + """ + @apiDefine csrf + @apiHeader {string="application/json"} Content-Type + The content type must be set to `application/json` to prevent CSRF attacks. + """ def dec(self, env, req, *args, **kwargs): if req.content_type and not req.content_type.startswith("application/json"): @@ -140,6 +146,64 @@ class API(object): return True, "" + """ + @api {post} /new create new + @apiGroup Comment + @apiUse csrf + + @apiParam {string} uri + The uri of the thread to create the comment on. + @apiParam {string} text + The comment’s raw text. + @apiParam {string} [author] + The comment’s author’s name. + @apiParam {string} [email] + The comment’s author’s email address. + @apiParam {string} [website] + The comment’s author’s website’s url. + @apiParam {number} [parent] + The parent comment’s id iff the new comment is a response to an existing comment. + + @apiExample {curl} Create a reply to comment with id 15: + curl 'https://comments.example.com/new?uri=/thread/' -d '{"text": "Stop saying that! *isso*!", "author": "Max Rant", "email": "rant@example.com", "parent": 15}' -H 'Content-Type: application/json' + + @apiSuccess {number} id + The comment’s id (assigned by the server). + @apiSuccess {number} parent + Id of the comment this comment is a reply to. `null` if this is a top-level-comment. + @apiSuccess {number=1,2,4} mode + The comment’s mode: + value | explanation + --- | --- + `1` | accepted: The comment was accepted by the server and is published. + `2` | in moderation queue: The comment was accepted by the server but awaits moderation. + `4` | deleted, but referenced: The comment was deleted on the server but is still referenced by replies. + @apiSuccess {string} author + The comments’s author’s name or `null`. + @apiSuccess {string} website + The comment’s author’s website or `null`. + @apiSuccess {string} hash + A hash uniquely identifying the comment’s author. + @apiSuccess {number} created + UNIX timestamp of the time the comment was created (on the server). + @apiSuccess {number} modified + UNIX timestamp of the time the comment was last modified (on the server). `null` if the comment was not yet modified. + + @apiSuccessExample Success after the above request: + { + "website": null, + "author": "Max Rant", + "parent": 15, + "created": 1464940838.254393, + "text": "<p>Stop saying that! <em>isso</em>!</p>", + "dislikes": 0, + "modified": null, + "mode": 1, + "hash": "e644f6ee43c0", + "id": 10, + "likes": 0 + } + """ @xhr @requires(str, 'uri') def new(self, environ, request, uri): From 1f804bcf8e2fb40cfc2f7b79e2837d3abee7296e Mon Sep 17 00:00:00 2001 From: Joshua Gleitze Date: Fri, 3 Jun 2016 10:28:51 +0200 Subject: [PATCH 04/78] apidoc for "view comment" --- isso/views/comments.py | 84 ++++++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index 8cf0cab..68ed6df 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -146,26 +146,14 @@ class API(object): return True, "" - """ - @api {post} /new create new - @apiGroup Comment - @apiUse csrf - - @apiParam {string} uri - The uri of the thread to create the comment on. - @apiParam {string} text - The comment’s raw text. - @apiParam {string} [author] - The comment’s author’s name. - @apiParam {string} [email] - The comment’s author’s email address. - @apiParam {string} [website] - The comment’s author’s website’s url. - @apiParam {number} [parent] - The parent comment’s id iff the new comment is a response to an existing comment. - - @apiExample {curl} Create a reply to comment with id 15: - curl 'https://comments.example.com/new?uri=/thread/' -d '{"text": "Stop saying that! *isso*!", "author": "Max Rant", "email": "rant@example.com", "parent": 15}' -H 'Content-Type: application/json' + # Common definitions for apidoc follow: + """ + @apiDefine plainParam + @apiParam {number=0,1} [plain] + Iff set to `1`, the plain text entered by the user will be returned in the comments’ `text` attribute (instead of the rendered markdown). + """ + """ + @apiDefine commentResponse @apiSuccess {number} id The comment’s id (assigned by the server). @@ -188,6 +176,30 @@ class API(object): UNIX timestamp of the time the comment was created (on the server). @apiSuccess {number} modified UNIX timestamp of the time the comment was last modified (on the server). `null` if the comment was not yet modified. + """ + + """ + @api {post} /new create new + @apiGroup Comment + @apiUse csrf + + @apiParam {string} uri + The uri of the thread to create the comment on. + @apiParam {string} text + The comment’s raw text. + @apiParam {string} [author] + The comment’s author’s name. + @apiParam {string} [email] + The comment’s author’s email address. + @apiParam {string} [website] + The comment’s author’s website’s url. + @apiParam {number} [parent] + The parent comment’s id iff the new comment is a response to an existing comment. + + @apiExample {curl} Create a reply to comment with id 15: + curl 'https://comments.example.com/new?uri=/thread/' -d '{"text": "Stop saying that! *isso*!", "author": "Max Rant", "email": "rant@example.com", "parent": 15}' -H 'Content-Type: application/json' + + @apiUse commentResponse @apiSuccessExample Success after the above request: { @@ -200,7 +212,7 @@ class API(object): "modified": null, "mode": 1, "hash": "e644f6ee43c0", - "id": 10, + "id": 23, "likes": 0 } """ @@ -277,6 +289,33 @@ class API(object): resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp + """ + @api {get} /id/:id view + @apiGroup Comment + + @apiParam {number} id + The id of the comment to view. + @apiUse plainParam + + @apiExample {curl} View the comment with id 4: + curl 'https://comments.example.com/id/4' + + @apiUse commentResponse + + @apiSuccessExample Example result: + { + "website": null, + "author": null, + "parent": null, + "created": 1464914341.312426, + "text": " <p>I want to use MySQL</p>", + "dislikes": 0, + "modified": null, + "mode": 1, + "id": 4, + "likes": 1 + } + """ def view(self, environ, request, id): rv = self.comments.get(id) @@ -421,8 +460,7 @@ class API(object): The URI of thread to get the comments from. @apiParam {number} [parent] Return only comments that are children of the comment with the provided ID. - @apiParam {number=0,1} [plain] - Iff set to `1`, the plain text entered by the user will be returned in the comments’ `text` attribute (instead of the rendered markdown). + @apiUse plainParam @apiParam {number} [limit] The maximum number of returned top-level comments. Omit for unlimited results. @apiParam {number} [nested_limit] From 9b79a98851939355faa58bd2594a698a9b45bc05 Mon Sep 17 00:00:00 2001 From: Joshua Gleitze Date: Fri, 3 Jun 2016 10:50:57 +0200 Subject: [PATCH 05/78] apidoc for "edit comment" --- isso/views/comments.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index 68ed6df..24e01fe 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -181,6 +181,8 @@ class API(object): """ @api {post} /new create new @apiGroup Comment + @apiDescription + Creates a new comment. The response will set a cookie on the requestor to enable them to later edit the comment. @apiUse csrf @apiParam {string} uri @@ -197,7 +199,7 @@ class API(object): The parent comment’s id iff the new comment is a response to an existing comment. @apiExample {curl} Create a reply to comment with id 15: - curl 'https://comments.example.com/new?uri=/thread/' -d '{"text": "Stop saying that! *isso*!", "author": "Max Rant", "email": "rant@example.com", "parent": 15}' -H 'Content-Type: application/json' + curl 'https://comments.example.com/new?uri=/thread/' -d '{"text": "Stop saying that! *isso*!", "author": "Max Rant", "email": "rant@example.com", "parent": 15}' -H 'Content-Type: application/json' -c cookie.txt @apiUse commentResponse @@ -330,6 +332,41 @@ class API(object): return JSON(rv, 200) + """ + @api {put} /id/:id edit + @apiGroup Comment + @apiDescription + Edit an existing comment. Editing a comment is only possible for a short period of time after it was created and only if the requestor has a valid cookie for it. See the [isso server documentation](https://posativ.org/isso/docs/configuration/server) for details. Editing a comment will set a new edit cookie in the response. + @apiUse csrf + + @apiParam {number} id + The id of the comment to edit. + @apiParam {string} text + A new (raw) text for the comment. + @apiParam {string} [author] + The modified comment’s author’s name. + @apiParam {string} [webiste] + The modified comment’s author’s website. + + @apiExample {curl} Edit comment with id 23: + curl -X PUT 'https://comments.example.com/id/23' -d {"text": "I see your point. However, I still disagree.", "website": "maxrant.important.com"} -H 'Content-Type: application/json' -b cookie.txt + + @apiUse commentResponse + + @apiSuccessExample Example response: + { + "website": "maxrant.important.com", + "author": "Max Rant", + "parent": 15, + "created": 1464940838.254393, + "text": "<p>I see your point. However, I still disagree.</p>", + "dislikes": 0, + "modified": 1464943439.073961, + "mode": 1, + "id": 23, + "likes": 0 + } + """ @xhr def edit(self, environ, request, id): From c3439e5c79081f7c450db0fcc99cea59b4945f5b Mon Sep 17 00:00:00 2001 From: Joshua Gleitze Date: Fri, 3 Jun 2016 11:06:46 +0200 Subject: [PATCH 06/78] apidoc for "delete comment" --- isso/views/comments.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/isso/views/comments.py b/isso/views/comments.py index 24e01fe..5f2bac7 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -411,6 +411,21 @@ class API(object): resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp + """ + @api {delete} '/id/:id' delete + @apiGroup Comment + @apiDescription + Delte an existing comment. Deleting a comment is only possible for a short period of time after it was created and only if the requestor has a valid cookie for it. See the [isso server documentation](https://posativ.org/isso/docs/configuration/server) for details. + + @apiParam {number} id + Id of the comment to delete. + + @apiExample {curl} Delete comment with id 14: + curl -X DELETE 'https://comments.example.com/id/14' -b cookie.txt + + @apiSuccessExample Successful deletion returns null: + null + """ @xhr def delete(self, environ, request, id, key=None): From ded4927ae40ceb49ef9b994737d796da43487fcd Mon Sep 17 00:00:00 2001 From: Joshua Gleitze Date: Fri, 3 Jun 2016 14:08:57 +0200 Subject: [PATCH 07/78] apidoc for moderate --- isso/views/comments.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/isso/views/comments.py b/isso/views/comments.py index 5f2bac7..2ad90e2 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -463,6 +463,40 @@ class API(object): resp.headers.add("X-Set-Cookie", cookie("isso-%i" % id)) return resp + """ + @api {post} /id/:id/:action/key moderate + @apiGroup Comment + @apiDescription + Publish or delete a comment that is in the moderation queue (mode `2`). In order to use this endpoint, the requestor needs a `key` that is usually obtained from an email sent out by isso. + + This endpoint can also be used with a `GET` request. In that case, a html page is returned that asks the user whether they are sure to perform the selected action. If they select “yes”, the query is repeated using `POST`. + + @apiParam {number} id + The id of the comment to moderate. + @apiParam {string=activate,delete} action + `activate` to publish the comment (change its mode to `1`). + `delete` to delete the comment + @apiParam {string} key + The moderation key to authenticate the moderation. + + @apiExample {curl} delete comment with id 13: + curl -X POST 'https://comments.example.com/id/13/delete/MTM.CjL6Fg.REIdVXa-whJS_x8ojQL4RrXnuF4' + + @apiSuccessExample {html} Using GET: + <!DOCTYPE html> + <html> + <head> + <script> + if (confirm('Delete: Are you sure?')) { + xhr = new XMLHttpRequest; + xhr.open('POST', window.location.href); + xhr.send(null); + } + </script> + + @apiSuccessExample Using POST: + Yo + """ def moderate(self, environ, request, id, action, key): try: From afd4107ac32637d0050e1505c5f616aed9ae9d18 Mon Sep 17 00:00:00 2001 From: Joshua Gleitze Date: Fri, 3 Jun 2016 14:09:16 +0200 Subject: [PATCH 08/78] apidoc for like & dislike --- isso/views/comments.py | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/isso/views/comments.py b/isso/views/comments.py index 2ad90e2..3096ab8 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -716,12 +716,60 @@ class API(object): return fetched_list + """ + @apiDefine likeResponse + @apiSuccess {number} likes + The (new) number of likes on the comment. + @apiSuccess {number} dislikes + The (new) number of dislikes on the comment. + """ + + """ + @api {post} /id/:id/like like + @apiGroup Comment + @apiDescription + Puts a “like” on a comment. The author of a comment cannot like its own comment. + + @apiParam {number} id + The id of the comment to like. + + @apiExample {curl} Like comment with id 23: + curl -X POST 'https://comments.example.com/id/23/like' + + @apiUse likeResponse + + @apiSuccessExample Example response + { + "likes": 5, + "dislikes": 2 + } + """ @xhr def like(self, environ, request, id): nv = self.comments.vote(True, id, utils.anonymize(str(request.remote_addr))) return JSON(nv, 200) + """ + @api {post} /id/:id/dislike dislike + @apiGroup Comment + @apiDescription + Puts a “dislike” on a comment. The author of a comment cannot dislike its own comment. + + @apiParam {number} id + The id of the comment to dislike. + + @apiExample {curl} Dislike comment with id 23: + curl -X POST 'https://comments.example.com/id/23/dislike' + + @apiUse likeResponse + + @apiSuccessExample Example response + { + "likes": 4, + "dislikes": 3 + } + """ @xhr def dislike(self, environ, request, id): From 8a9fe29bce19957b5c94ce4ce6c2ebfc45fc1896 Mon Sep 17 00:00:00 2001 From: Joshua Gleitze Date: Fri, 3 Jun 2016 14:20:36 +0200 Subject: [PATCH 09/78] apidoc for count --- isso/views/comments.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/isso/views/comments.py b/isso/views/comments.py index 3096ab8..868dbfe 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -787,6 +787,18 @@ class API(object): return JSON(rv, 200) + """ + @api {post} /count count comments + @apiGroup Thread + @apiDescription + Counts the number of comments on multiple threads. The requestor provides a list of thread uris. The number of comments on each thread is returned as a list, in the same order as the threads were requested. The counts include comments that are reponses to comments. + + @apiExample {curl} get the count of 5 threads: + curl 'https://comments.example.com/count' -d '["/blog/firstPost.html", "/blog/controversalPost.html", "/blog/howToCode.html", "/blog/boringPost.html", "/blog/isso.html"] + + @apiSuccessExample Counts of 5 threads: + [2, 18, 4, 0, 3] + """ def counts(self, environ, request): data = request.get_json() From 084f6e5cf050222c35ace7c4f8c95556d0d19c1e Mon Sep 17 00:00:00 2001 From: Joshua Gleitze Date: Fri, 3 Jun 2016 14:39:22 +0200 Subject: [PATCH 10/78] apidoc settings --- apidoc.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apidoc.json b/apidoc.json index 760e429..a960a32 100644 --- a/apidoc.json +++ b/apidoc.json @@ -1,6 +1,11 @@ { "name": "isso", "description": "a Disqus alternative", - "title": "isso API" + "title": "isso API", + "order": ["Thread", "Comment"], + "template": { + "withCompare": false + } + } From 2a11c000d49d1749c1091cc55102909b78ebedcb Mon Sep 17 00:00:00 2001 From: Joshua Gleitze Date: Fri, 3 Jun 2016 20:04:58 +0200 Subject: [PATCH 11/78] convert bad tabs to spaces --- isso/views/comments.py | 404 ++++++++++++++++++++--------------------- 1 file changed, 202 insertions(+), 202 deletions(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index 868dbfe..6a7aef1 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -61,11 +61,11 @@ def xhr(func): """ - """ - @apiDefine csrf - @apiHeader {string="application/json"} Content-Type - The content type must be set to `application/json` to prevent CSRF attacks. - """ + """ + @apiDefine csrf + @apiHeader {string="application/json"} Content-Type + The content type must be set to `application/json` to prevent CSRF attacks. + """ def dec(self, env, req, *args, **kwargs): if req.content_type and not req.content_type.startswith("application/json"): @@ -146,44 +146,44 @@ class API(object): return True, "" - # Common definitions for apidoc follow: - """ - @apiDefine plainParam - @apiParam {number=0,1} [plain] - Iff set to `1`, the plain text entered by the user will be returned in the comments’ `text` attribute (instead of the rendered markdown). - """ - """ - @apiDefine commentResponse + # Common definitions for apidoc follow: + """ + @apiDefine plainParam + @apiParam {number=0,1} [plain] + Iff set to `1`, the plain text entered by the user will be returned in the comments’ `text` attribute (instead of the rendered markdown). + """ + """ + @apiDefine commentResponse - @apiSuccess {number} id - The comment’s id (assigned by the server). - @apiSuccess {number} parent - Id of the comment this comment is a reply to. `null` if this is a top-level-comment. - @apiSuccess {number=1,2,4} mode - The comment’s mode: - value | explanation - --- | --- - `1` | accepted: The comment was accepted by the server and is published. - `2` | in moderation queue: The comment was accepted by the server but awaits moderation. - `4` | deleted, but referenced: The comment was deleted on the server but is still referenced by replies. - @apiSuccess {string} author - The comments’s author’s name or `null`. - @apiSuccess {string} website - The comment’s author’s website or `null`. - @apiSuccess {string} hash - A hash uniquely identifying the comment’s author. - @apiSuccess {number} created - UNIX timestamp of the time the comment was created (on the server). - @apiSuccess {number} modified - UNIX timestamp of the time the comment was last modified (on the server). `null` if the comment was not yet modified. - """ + @apiSuccess {number} id + The comment’s id (assigned by the server). + @apiSuccess {number} parent + Id of the comment this comment is a reply to. `null` if this is a top-level-comment. + @apiSuccess {number=1,2,4} mode + The comment’s mode: + value | explanation + --- | --- + `1` | accepted: The comment was accepted by the server and is published. + `2` | in moderation queue: The comment was accepted by the server but awaits moderation. + `4` | deleted, but referenced: The comment was deleted on the server but is still referenced by replies. + @apiSuccess {string} author + The comments’s author’s name or `null`. + @apiSuccess {string} website + The comment’s author’s website or `null`. + @apiSuccess {string} hash + A hash uniquely identifying the comment’s author. + @apiSuccess {number} created + UNIX timestamp of the time the comment was created (on the server). + @apiSuccess {number} modified + UNIX timestamp of the time the comment was last modified (on the server). `null` if the comment was not yet modified. + """ """ @api {post} /new create new @apiGroup Comment - @apiDescription - Creates a new comment. The response will set a cookie on the requestor to enable them to later edit the comment. - @apiUse csrf + @apiDescription + Creates a new comment. The response will set a cookie on the requestor to enable them to later edit the comment. + @apiUse csrf @apiParam {string} uri The uri of the thread to create the comment on. @@ -198,25 +198,25 @@ class API(object): @apiParam {number} [parent] The parent comment’s id iff the new comment is a response to an existing comment. - @apiExample {curl} Create a reply to comment with id 15: - curl 'https://comments.example.com/new?uri=/thread/' -d '{"text": "Stop saying that! *isso*!", "author": "Max Rant", "email": "rant@example.com", "parent": 15}' -H 'Content-Type: application/json' -c cookie.txt + @apiExample {curl} Create a reply to comment with id 15: + curl 'https://comments.example.com/new?uri=/thread/' -d '{"text": "Stop saying that! *isso*!", "author": "Max Rant", "email": "rant@example.com", "parent": 15}' -H 'Content-Type: application/json' -c cookie.txt - @apiUse commentResponse + @apiUse commentResponse - @apiSuccessExample Success after the above request: - { - "website": null, - "author": "Max Rant", - "parent": 15, - "created": 1464940838.254393, - "text": "<p>Stop saying that! <em>isso</em>!</p>", - "dislikes": 0, - "modified": null, - "mode": 1, - "hash": "e644f6ee43c0", - "id": 23, - "likes": 0 - } + @apiSuccessExample Success after the above request: + { + "website": null, + "author": "Max Rant", + "parent": 15, + "created": 1464940838.254393, + "text": "<p>Stop saying that! <em>isso</em>!</p>", + "dislikes": 0, + "modified": null, + "mode": 1, + "hash": "e644f6ee43c0", + "id": 23, + "likes": 0 + } """ @xhr @requires(str, 'uri') @@ -292,32 +292,32 @@ class API(object): return resp """ - @api {get} /id/:id view - @apiGroup Comment + @api {get} /id/:id view + @apiGroup Comment - @apiParam {number} id - The id of the comment to view. - @apiUse plainParam + @apiParam {number} id + The id of the comment to view. + @apiUse plainParam - @apiExample {curl} View the comment with id 4: - curl 'https://comments.example.com/id/4' + @apiExample {curl} View the comment with id 4: + curl 'https://comments.example.com/id/4' - @apiUse commentResponse + @apiUse commentResponse - @apiSuccessExample Example result: - { - "website": null, - "author": null, - "parent": null, - "created": 1464914341.312426, - "text": " <p>I want to use MySQL</p>", - "dislikes": 0, - "modified": null, - "mode": 1, - "id": 4, - "likes": 1 - } - """ + @apiSuccessExample Example result: + { + "website": null, + "author": null, + "parent": null, + "created": 1464914341.312426, + "text": " <p>I want to use MySQL</p>", + "dislikes": 0, + "modified": null, + "mode": 1, + "id": 4, + "likes": 1 + } + """ def view(self, environ, request, id): rv = self.comments.get(id) @@ -332,41 +332,41 @@ class API(object): return JSON(rv, 200) - """ - @api {put} /id/:id edit - @apiGroup Comment + """ + @api {put} /id/:id edit + @apiGroup Comment @apiDescription - Edit an existing comment. Editing a comment is only possible for a short period of time after it was created and only if the requestor has a valid cookie for it. See the [isso server documentation](https://posativ.org/isso/docs/configuration/server) for details. Editing a comment will set a new edit cookie in the response. - @apiUse csrf + Edit an existing comment. Editing a comment is only possible for a short period of time after it was created and only if the requestor has a valid cookie for it. See the [isso server documentation](https://posativ.org/isso/docs/configuration/server) for details. Editing a comment will set a new edit cookie in the response. + @apiUse csrf - @apiParam {number} id - The id of the comment to edit. - @apiParam {string} text - A new (raw) text for the comment. - @apiParam {string} [author] - The modified comment’s author’s name. - @apiParam {string} [webiste] - The modified comment’s author’s website. + @apiParam {number} id + The id of the comment to edit. + @apiParam {string} text + A new (raw) text for the comment. + @apiParam {string} [author] + The modified comment’s author’s name. + @apiParam {string} [webiste] + The modified comment’s author’s website. - @apiExample {curl} Edit comment with id 23: - curl -X PUT 'https://comments.example.com/id/23' -d {"text": "I see your point. However, I still disagree.", "website": "maxrant.important.com"} -H 'Content-Type: application/json' -b cookie.txt + @apiExample {curl} Edit comment with id 23: + curl -X PUT 'https://comments.example.com/id/23' -d {"text": "I see your point. However, I still disagree.", "website": "maxrant.important.com"} -H 'Content-Type: application/json' -b cookie.txt - @apiUse commentResponse + @apiUse commentResponse - @apiSuccessExample Example response: - { - "website": "maxrant.important.com", - "author": "Max Rant", - "parent": 15, - "created": 1464940838.254393, - "text": "<p>I see your point. However, I still disagree.</p>", - "dislikes": 0, - "modified": 1464943439.073961, - "mode": 1, - "id": 23, - "likes": 0 - } - """ + @apiSuccessExample Example response: + { + "website": "maxrant.important.com", + "author": "Max Rant", + "parent": 15, + "created": 1464940838.254393, + "text": "<p>I see your point. However, I still disagree.</p>", + "dislikes": 0, + "modified": 1464943439.073961, + "mode": 1, + "id": 23, + "likes": 0 + } + """ @xhr def edit(self, environ, request, id): @@ -411,21 +411,21 @@ class API(object): resp.headers.add("X-Set-Cookie", cookie("isso-%i" % rv["id"])) return resp - """ - @api {delete} '/id/:id' delete - @apiGroup Comment - @apiDescription - Delte an existing comment. Deleting a comment is only possible for a short period of time after it was created and only if the requestor has a valid cookie for it. See the [isso server documentation](https://posativ.org/isso/docs/configuration/server) for details. + """ + @api {delete} '/id/:id' delete + @apiGroup Comment + @apiDescription + Delte an existing comment. Deleting a comment is only possible for a short period of time after it was created and only if the requestor has a valid cookie for it. See the [isso server documentation](https://posativ.org/isso/docs/configuration/server) for details. - @apiParam {number} id - Id of the comment to delete. + @apiParam {number} id + Id of the comment to delete. - @apiExample {curl} Delete comment with id 14: - curl -X DELETE 'https://comments.example.com/id/14' -b cookie.txt + @apiExample {curl} Delete comment with id 14: + curl -X DELETE 'https://comments.example.com/id/14' -b cookie.txt - @apiSuccessExample Successful deletion returns null: - null - """ + @apiSuccessExample Successful deletion returns null: + null + """ @xhr def delete(self, environ, request, id, key=None): @@ -463,40 +463,40 @@ class API(object): resp.headers.add("X-Set-Cookie", cookie("isso-%i" % id)) return resp - """ - @api {post} /id/:id/:action/key moderate - @apiGroup Comment - @apiDescription - Publish or delete a comment that is in the moderation queue (mode `2`). In order to use this endpoint, the requestor needs a `key` that is usually obtained from an email sent out by isso. + """ + @api {post} /id/:id/:action/key moderate + @apiGroup Comment + @apiDescription + Publish or delete a comment that is in the moderation queue (mode `2`). In order to use this endpoint, the requestor needs a `key` that is usually obtained from an email sent out by isso. - This endpoint can also be used with a `GET` request. In that case, a html page is returned that asks the user whether they are sure to perform the selected action. If they select “yes”, the query is repeated using `POST`. + This endpoint can also be used with a `GET` request. In that case, a html page is returned that asks the user whether they are sure to perform the selected action. If they select “yes”, the query is repeated using `POST`. - @apiParam {number} id - The id of the comment to moderate. - @apiParam {string=activate,delete} action - `activate` to publish the comment (change its mode to `1`). - `delete` to delete the comment - @apiParam {string} key - The moderation key to authenticate the moderation. + @apiParam {number} id + The id of the comment to moderate. + @apiParam {string=activate,delete} action + `activate` to publish the comment (change its mode to `1`). + `delete` to delete the comment + @apiParam {string} key + The moderation key to authenticate the moderation. - @apiExample {curl} delete comment with id 13: - curl -X POST 'https://comments.example.com/id/13/delete/MTM.CjL6Fg.REIdVXa-whJS_x8ojQL4RrXnuF4' + @apiExample {curl} delete comment with id 13: + curl -X POST 'https://comments.example.com/id/13/delete/MTM.CjL6Fg.REIdVXa-whJS_x8ojQL4RrXnuF4' - @apiSuccessExample {html} Using GET: - <!DOCTYPE html> - <html> - <head> - <script> - if (confirm('Delete: Are you sure?')) { - xhr = new XMLHttpRequest; - xhr.open('POST', window.location.href); - xhr.send(null); - } - </script> + @apiSuccessExample {html} Using GET: + <!DOCTYPE html> + <html> + <head> + <script> + if (confirm('Delete: Are you sure?')) { + xhr = new XMLHttpRequest; + xhr.open('POST', window.location.href); + xhr.send(null); + } + </script> - @apiSuccessExample Using POST: - Yo - """ + @apiSuccessExample Using POST: + Yo + """ def moderate(self, environ, request, id, action, key): try: @@ -546,7 +546,7 @@ class API(object): The URI of thread to get the comments from. @apiParam {number} [parent] Return only comments that are children of the comment with the provided ID. - @apiUse plainParam + @apiUse plainParam @apiParam {number} [limit] The maximum number of returned top-level comments. Omit for unlimited results. @apiParam {number} [nested_limit] @@ -555,13 +555,13 @@ class API(object): Includes only comments were added after the provided UNIX timestamp. @apiSuccess {number} total_replies - The number of replies if the `limit` parameter was not set. If `after` is set to `X`, this is the number of comments that were created after `X`. So setting `after` may change this value! + The number of replies if the `limit` parameter was not set. If `after` is set to `X`, this is the number of comments that were created after `X`. So setting `after` may change this value! @apiSuccess {Object[]} replies - The list of comments. Each comment also has the `total_replies`, `replies`, `id` and `hidden_replies` properties to represent nested comments. + The list of comments. Each comment also has the `total_replies`, `replies`, `id` and `hidden_replies` properties to represent nested comments. @apiSuccess {number} id - Id of the comment `replies` is the list of replies of. `null` for the list of toplevel comments. + Id of the comment `replies` is the list of replies of. `null` for the list of toplevel comments. @apiSuccess {number} hidden_replies - The number of comments that were ommited from the results because of the `limit` request parameter. Usually, this will be `total_replies` - `limit`. + The number of comments that were ommited from the results because of the `limit` request parameter. Usually, this will be `total_replies` - `limit`. @apiExample {curl} Get 2 comments with 5 responses: curl 'https://comments.example.com/?uri=/thread/&limit=2&nested_limit=5' @@ -716,60 +716,60 @@ class API(object): return fetched_list - """ - @apiDefine likeResponse - @apiSuccess {number} likes - The (new) number of likes on the comment. - @apiSuccess {number} dislikes - The (new) number of dislikes on the comment. - """ + """ + @apiDefine likeResponse + @apiSuccess {number} likes + The (new) number of likes on the comment. + @apiSuccess {number} dislikes + The (new) number of dislikes on the comment. + """ - """ - @api {post} /id/:id/like like - @apiGroup Comment - @apiDescription - Puts a “like” on a comment. The author of a comment cannot like its own comment. + """ + @api {post} /id/:id/like like + @apiGroup Comment + @apiDescription + Puts a “like” on a comment. The author of a comment cannot like its own comment. - @apiParam {number} id - The id of the comment to like. + @apiParam {number} id + The id of the comment to like. - @apiExample {curl} Like comment with id 23: - curl -X POST 'https://comments.example.com/id/23/like' + @apiExample {curl} Like comment with id 23: + curl -X POST 'https://comments.example.com/id/23/like' - @apiUse likeResponse + @apiUse likeResponse - @apiSuccessExample Example response - { - "likes": 5, - "dislikes": 2 - } - """ + @apiSuccessExample Example response + { + "likes": 5, + "dislikes": 2 + } + """ @xhr def like(self, environ, request, id): nv = self.comments.vote(True, id, utils.anonymize(str(request.remote_addr))) return JSON(nv, 200) - """ - @api {post} /id/:id/dislike dislike - @apiGroup Comment - @apiDescription - Puts a “dislike” on a comment. The author of a comment cannot dislike its own comment. + """ + @api {post} /id/:id/dislike dislike + @apiGroup Comment + @apiDescription + Puts a “dislike” on a comment. The author of a comment cannot dislike its own comment. - @apiParam {number} id - The id of the comment to dislike. + @apiParam {number} id + The id of the comment to dislike. - @apiExample {curl} Dislike comment with id 23: - curl -X POST 'https://comments.example.com/id/23/dislike' + @apiExample {curl} Dislike comment with id 23: + curl -X POST 'https://comments.example.com/id/23/dislike' - @apiUse likeResponse + @apiUse likeResponse - @apiSuccessExample Example response - { - "likes": 4, - "dislikes": 3 - } - """ + @apiSuccessExample Example response + { + "likes": 4, + "dislikes": 3 + } + """ @xhr def dislike(self, environ, request, id): @@ -787,18 +787,18 @@ class API(object): return JSON(rv, 200) - """ - @api {post} /count count comments - @apiGroup Thread - @apiDescription - Counts the number of comments on multiple threads. The requestor provides a list of thread uris. The number of comments on each thread is returned as a list, in the same order as the threads were requested. The counts include comments that are reponses to comments. + """ + @api {post} /count count comments + @apiGroup Thread + @apiDescription + Counts the number of comments on multiple threads. The requestor provides a list of thread uris. The number of comments on each thread is returned as a list, in the same order as the threads were requested. The counts include comments that are reponses to comments. - @apiExample {curl} get the count of 5 threads: - curl 'https://comments.example.com/count' -d '["/blog/firstPost.html", "/blog/controversalPost.html", "/blog/howToCode.html", "/blog/boringPost.html", "/blog/isso.html"] + @apiExample {curl} get the count of 5 threads: + curl 'https://comments.example.com/count' -d '["/blog/firstPost.html", "/blog/controversalPost.html", "/blog/howToCode.html", "/blog/boringPost.html", "/blog/isso.html"] - @apiSuccessExample Counts of 5 threads: - [2, 18, 4, 0, 3] - """ + @apiSuccessExample Counts of 5 threads: + [2, 18, 4, 0, 3] + """ def counts(self, environ, request): data = request.get_json() From 09b69feae91a89f9e56f63ee77812174f7a4c495 Mon Sep 17 00:00:00 2001 From: ivegotasthma Date: Sat, 17 Dec 2016 22:42:07 +0100 Subject: [PATCH 12/78] fix: add missing i18n entry Fixes an indexing error a user gets when he tries to make the language of isso to `bg` --- isso/js/app/i18n.js | 1 + 1 file changed, 1 insertion(+) diff --git a/isso/js/app/i18n.js b/isso/js/app/i18n.js index 98141f6..43a5f52 100644 --- a/isso/js/app/i18n.js +++ b/isso/js/app/i18n.js @@ -55,6 +55,7 @@ define(["app/config", "app/i18n/bg", "app/i18n/cs", "app/i18n/de", } var catalogue = { + bg: bg, cs: cs, de: de, el: el, From 03b0de2d81f7fec9da98cda8881736fdac244339 Mon Sep 17 00:00:00 2001 From: Alexandre Perrin Date: Fri, 6 Jan 2017 15:01:58 +0100 Subject: [PATCH 13/78] api.rst: JSON and english typos --- docs/docs/extras/api.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs/extras/api.rst b/docs/docs/extras/api.rst index 4c9f1d7..348a256 100644 --- a/docs/docs/extras/api.rst +++ b/docs/docs/extras/api.rst @@ -23,11 +23,11 @@ Isso: "mode": 1, "hash": "4505c1eeda98", "author": null, - "website": null + "website": null, "created": 1387321261.572392, "modified": null, "likes": 3, - "dislikes": 0, + "dislikes": 0 } id : @@ -70,7 +70,7 @@ modified : List comments ------------- -List all publicely visible comments for thread `uri`: +List all publicly visible comments for thread `uri`: .. code-block:: text From cd460ef152d63ffe0dbc008b6c3cadb52977a255 Mon Sep 17 00:00:00 2001 From: "Mads R. Havmand" Date: Sat, 7 Jan 2017 19:02:00 +0100 Subject: [PATCH 14/78] Danish translation --- isso/js/app/i18n/da.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 isso/js/app/i18n/da.js diff --git a/isso/js/app/i18n/da.js b/isso/js/app/i18n/da.js new file mode 100644 index 0000000..eb64fc4 --- /dev/null +++ b/isso/js/app/i18n/da.js @@ -0,0 +1,30 @@ +define({ + "postbox-text": "Type Comment Here (at least 3 chars)", + "postbox-author": "Name (optional)", + "postbox-email": "E-mail (optional)", + "postbox-website": "Website (optional)", + "postbox-submit": "Submit", + + "num-comments": "One Comment\n{{ n }} Comments", + "no-comments": "Ingen kommentarer endnu", + + "comment-reply": "Svar", + "comment-edit": "Rediger", + "comment-save": "Gem", + "comment-delete": "Fjern", + "comment-confirm": "Bekræft", + "comment-close": "Luk", + "comment-cancel": "Annuller", + "comment-deleted": "Kommentar slettet.", + "comment-queued": "Kommentar i kø for moderation.", + "comment-anonymous": "Anonym", + "comment-hidden": "{{ n }} Skjult", + + "date-now": "lige nu", + "date-minute": "et minut siden\n{{ n }} minutter siden", + "date-hour": "en time siden\n{{ n }} timer siden", + "date-day": "Igår\n{{ n }} dage siden", + "date-week": "sidste uge\n{{ n }} uger siden", + "date-month": "sidste måned\n{{ n }} måneder siden", + "date-year": "sidste år\n{{ n }} år siden" +}); From 45a481daeb6226b3cd2e9fd2cb1a636c6601ee41 Mon Sep 17 00:00:00 2001 From: Pelle Nilsson Date: Sun, 19 Mar 2017 18:42:03 +0100 Subject: [PATCH 15/78] Fix require-email setting, #308 --- isso/js/app/isso.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isso/js/app/isso.js b/isso/js/app/isso.js index a46f43a..1875351 100644 --- a/isso/js/app/isso.js +++ b/isso/js/app/isso.js @@ -35,8 +35,8 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", // email is not optional if this config parameter is set if (config["require-email"]) { - $("[name='email']", el).placeholder = - $("[name='email']", el).placeholder.replace(/ \(.*\)/, ""); + $("[name='email']", el).setAttribute("placeholder", + $("[name='email']", el).getAttribute("placeholder").replace(/ \(.*\)/, "")); } // submit form, initialize optional fields with `null` and reset form. From 88aafa60e5f067317e26e1a99e87088fbfdbf27a Mon Sep 17 00:00:00 2001 From: Graham Inggs Date: Mon, 27 Mar 2017 17:44:02 +0200 Subject: [PATCH 16/78] add SOURCELINK_SUFFIX for compatibility with Sphinx 1.5 With Sphinx 1.5, this is needed by searchtools.js to display the source snippets (see sphinx-doc/sphinx#2454). With earlier Sphinx versions, this is a no-op because the undefined variable will evaluate to an empty string. --- docs/_isso/layout.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/_isso/layout.html b/docs/_isso/layout.html index 2462c30..c0f9476 100644 --- a/docs/_isso/layout.html +++ b/docs/_isso/layout.html @@ -10,7 +10,8 @@ VERSION: '{{ release|e }}', COLLAPSE_INDEX: false, FILE_SUFFIX: '{{ '' if no_search_suffix else file_suffix }}', - HAS_SOURCE: {{ has_source|lower }} + HAS_SOURCE: {{ has_source|lower }}, + SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}' }; {%- for scriptfile in script_files %} From 4b7a32afaceb3a835f04c463611d1fa572fb6ea3 Mon Sep 17 00:00:00 2001 From: Shengbin Meng Date: Tue, 28 Mar 2017 21:35:51 +0800 Subject: [PATCH 17/78] Make the Chinese translations actually work Before this change, the user must configure `lang="zh"` to use the CN version (neither `lang="zh_CN"` nor `lang="zh_TW"` works). --- isso/js/app/i18n.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/isso/js/app/i18n.js b/isso/js/app/i18n.js index 98141f6..1ec5ca2 100644 --- a/isso/js/app/i18n.js +++ b/isso/js/app/i18n.js @@ -2,8 +2,8 @@ define(["app/config", "app/i18n/bg", "app/i18n/cs", "app/i18n/de", "app/i18n/en", "app/i18n/fa", "app/i18n/fi", "app/i18n/fr", "app/i18n/hr", "app/i18n/ru", "app/i18n/it", "app/i18n/eo", "app/i18n/sv", "app/i18n/nl", "app/i18n/el_GR", "app/i18n/es", - "app/i18n/vi", "app/i18n/zh_CN"], - function(config, bg, cs, de, en, fa, fi, fr, hr, ru, it, eo, sv, nl, el, es, vi, zh) { + "app/i18n/vi", "app/i18n/zh_CN", "app/i18n/zh_TW"], + function(config, bg, cs, de, en, fa, fi, fr, hr, ru, it, eo, sv, nl, el, es, vi, zh_CN, zh_TW) { "use strict"; @@ -23,7 +23,8 @@ define(["app/config", "app/i18n/bg", "app/i18n/cs", "app/i18n/de", case "sv": case "nl": case "vi": - case "zh": + case "zh_CN": + case "zh_TW": return function(msgs, n) { return msgs[n === 1 ? 0 : 1]; }; @@ -70,7 +71,8 @@ define(["app/config", "app/i18n/bg", "app/i18n/cs", "app/i18n/de", sv: sv, nl: nl, vi: vi, - zh: zh + zh_CN: zh_CN, + zh_TW: zh_TW }; var plural = pluralforms(lang); From a9500e29dd5cd9c7bcb0459267e6b92e9f0ec602 Mon Sep 17 00:00:00 2001 From: Shengbin Meng Date: Tue, 28 Mar 2017 21:55:54 +0800 Subject: [PATCH 18/78] Add language "zh" as alias of "zh_CN" --- isso/js/app/i18n.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/isso/js/app/i18n.js b/isso/js/app/i18n.js index 1ec5ca2..461cbdb 100644 --- a/isso/js/app/i18n.js +++ b/isso/js/app/i18n.js @@ -2,8 +2,8 @@ define(["app/config", "app/i18n/bg", "app/i18n/cs", "app/i18n/de", "app/i18n/en", "app/i18n/fa", "app/i18n/fi", "app/i18n/fr", "app/i18n/hr", "app/i18n/ru", "app/i18n/it", "app/i18n/eo", "app/i18n/sv", "app/i18n/nl", "app/i18n/el_GR", "app/i18n/es", - "app/i18n/vi", "app/i18n/zh_CN", "app/i18n/zh_TW"], - function(config, bg, cs, de, en, fa, fi, fr, hr, ru, it, eo, sv, nl, el, es, vi, zh_CN, zh_TW) { + "app/i18n/vi", "app/i18n/zh_CN", "app/i18n/zh_CN", "app/i18n/zh_TW"], + function(config, bg, cs, de, en, fa, fi, fr, hr, ru, it, eo, sv, nl, el, es, vi, zh, zh_CN, zh_TW) { "use strict"; @@ -23,6 +23,7 @@ define(["app/config", "app/i18n/bg", "app/i18n/cs", "app/i18n/de", case "sv": case "nl": case "vi": + case "zh": case "zh_CN": case "zh_TW": return function(msgs, n) { @@ -71,6 +72,7 @@ define(["app/config", "app/i18n/bg", "app/i18n/cs", "app/i18n/de", sv: sv, nl: nl, vi: vi, + zh: zh_CN, zh_CN: zh_CN, zh_TW: zh_TW }; From 0a93c866ff7baa42de72f03cd827e0607b888b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Sun, 5 Jun 2016 01:10:08 +0200 Subject: [PATCH 19/78] Add a basic admin interface (Fix issue #10) Add a basic admin interface (Fix issue #10) wip again still wip fix login page --- isso/__init__.py | 1 + isso/css/admin.css | 115 ++++++++++++++++++++++++++++ isso/db/comments.py | 59 ++++++++++++++- isso/img/isso.svg | 2 + isso/templates/admin.html | 154 ++++++++++++++++++++++++++++++++++++++ isso/templates/login.html | 30 ++++++++ isso/utils/__init__.py | 18 ++++- isso/views/comments.py | 47 +++++++++++- setup.py | 2 +- share/isso-dev.conf | 1 + share/isso.conf | 5 +- 11 files changed, 427 insertions(+), 7 deletions(-) create mode 100644 isso/css/admin.css create mode 100644 isso/img/isso.svg create mode 100644 isso/templates/admin.html create mode 100644 isso/templates/login.html diff --git a/isso/__init__.py b/isso/__init__.py index 7795c4f..f842882 100644 --- a/isso/__init__.py +++ b/isso/__init__.py @@ -186,6 +186,7 @@ def make_app(conf=None, threading=True, multiprocessing=False, uwsgi=False): wrapper.append(partial(SharedDataMiddleware, exports={ '/js': join(dirname(__file__), 'js/'), '/css': join(dirname(__file__), 'css/'), + '/img': join(dirname(__file__), 'img/'), '/demo': join(dirname(__file__), 'demo/') })) diff --git a/isso/css/admin.css b/isso/css/admin.css new file mode 100644 index 0000000..6f747b9 --- /dev/null +++ b/isso/css/admin.css @@ -0,0 +1,115 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} +h1, h2, h3, h4, h5, h6 { + font-style: normal; + font-weight: normal; +} +input { + text-align: center; +} +.header::before, .header::after { + content: " "; + display: table; +} +.header::after { + clear: both; +} +.header::before, .header::after { + content: " "; + display: table; +} +.header { + margin-left: auto; + margin-right: auto; + max-width: 68em; + padding-bottom: 1em; + padding-top: 1em; +} +.header header { + display: block; + float: left; + font-weight: normal; + margin-right: 16.0363%; + width: 41.9818%; +} +.header header .logo { + float: left; + max-height: 60px; + padding-right: 12px; +} +.header header h1 { + font-size: 1.55em; + margin-bottom: 0.3em; +} +.header header h2 { + font-size: 1.05em; +} +.header a, .header a:visited { + color: #4d4c4c; + text-decoration: none; +} +.outer { + background-color: #eeeeee; + box-shadow: 0 0 0.5em #c0c0c0 inset; +} +.outer .filters::before, .outer .filters::after { + content: " "; + display: table; +} +.outer .filters::after { + clear: both; +} +.outer .filters::before, .outer .filters::after { + content: " "; + display: table; +} +.outer .filters { + margin-left: auto; + margin-right: auto; + max-width: 68em; + padding: 1em; +} + +a { + text-decoration: none; + color: #4d4c4c; +} +.label { + background-color: #ddd; + border: 1px solid #ccc; + border-radius: 2px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + cursor: pointer; + line-height: 1.4em; + outline: 0 none; + padding: calc(0.6em - 1px); +} +.active { + box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6) inset; +} +.label-valid { + background-color: #cfc; + border-color: #cfc; +} +.label-pending { + background-color: #ffc; + border-color: #ffc; +} +.mode { + float: left; +} +.pagination { + float: right; +} +.note .label { + margin: 9px; + padding: 3px; +} +#login { + margin-top: 40px; + text-align: center; + width: 100%; +} diff --git a/isso/db/comments.py b/isso/db/comments.py index 496a4e5..69790ac 100644 --- a/isso/db/comments.py +++ b/isso/db/comments.py @@ -19,8 +19,11 @@ class Comments: The tuple (tid, id) is unique and thus primary key. """ - fields = ['tid', 'id', 'parent', 'created', 'modified', 'mode', 'remote_addr', - 'text', 'author', 'email', 'website', 'likes', 'dislikes', 'voters'] + fields = ['tid', 'id', 'parent', 'created', 'modified', + 'mode', # status of the comment 1 = valid, 2 = pending, + # 4 = soft-deleted (cannot hard delete because of replies) + 'remote_addr', 'text', 'author', 'email', 'website', + 'likes', 'dislikes', 'voters'] def __init__(self, db): @@ -97,6 +100,58 @@ class Comments: return None + def count_modes(self): + """ + Return comment mode counts for admin + """ + comment_count = self.db.execute( + 'SELECT mode, COUNT(comments.id) FROM comments ' + 'GROUP BY comments.mode').fetchall() + return dict(comment_count) + + def fetchall(self, mode=5, after=0, parent='any', order_by='id', + limit=100, page=0): + """ + Return comments for admin with :param:`mode`. + """ + fields_comments = ['tid', 'id', 'parent', 'created', 'modified', + 'mode', 'remote_addr', 'text', 'author', + 'email', 'website', 'likes', 'dislikes'] + fields_threads = ['uri', 'title'] + sql_comments_fields = ', '.join(['comments.' + f + for f in fields_comments]) + sql_threads_fields = ', '.join(['threads.' + f + for f in fields_threads]) + sql = ['SELECT ' + sql_comments_fields + ', ' + \ + sql_threads_fields + ' ' + 'FROM comments INNER JOIN threads ' + 'ON comments.tid=threads.id ' + 'WHERE comments.mode = ? '] + sql_args = [mode] + + if parent != 'any': + if parent is None: + sql.append('AND comments.parent IS NULL') + else: + sql.append('AND comments.parent=?') + sql_args.append(parent) + + # custom sanitization + if order_by not in ['id', 'created', 'modified', 'likes', 'dislikes']: + order_by = 'id' + sql.append('ORDER BY ') + sql.append('comments.' + order_by) + sql.append(' DESC') + + if limit: + sql.append('LIMIT ?,?') + sql_args.append(page * limit) + sql_args.append(limit) + + rv = self.db.execute(sql, sql_args).fetchall() + for item in rv: + yield dict(zip(fields_comments + fields_threads, item)) + def fetch(self, uri, mode=5, after=0, parent='any', order_by='id', limit=None): """ Return comments for :param:`uri` with :param:`mode`. diff --git a/isso/img/isso.svg b/isso/img/isso.svg new file mode 100644 index 0000000..1aba2dd --- /dev/null +++ b/isso/img/isso.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/isso/templates/admin.html b/isso/templates/admin.html new file mode 100644 index 0000000..f3d9a53 --- /dev/null +++ b/isso/templates/admin.html @@ -0,0 +1,154 @@ + + + Isso admin + + + + + +
+ +
+
+ + +
+
+
+ {% for comment in comments %} +
+ {% if conf.avatar %} +
+ svg(data-hash='#{{comment.hash}}') +
+ {% endif %} +
+
+ {% if comment.author %} + {{comment.author}} + {% else %} + Anonymous + {% endif %} + {% if comment.website %} + ({{comment.website}}) + {% endif %} + + + + {% if comment.mode == 1 %} + Valid + {% elif comment.mode == 2 %} + Pending + {% elif comment.mode == 4 %} + Staled + {% endif %} + +
+
+ {% if comment.mode == 4 %} + HIDDEN. Original text:
+ {% endif %} + {{comment.text}} +
+ +
+
+ {% endfor %} +
+
+ + diff --git a/isso/templates/login.html b/isso/templates/login.html new file mode 100644 index 0000000..a9aa888 --- /dev/null +++ b/isso/templates/login.html @@ -0,0 +1,30 @@ + + + Isso admin + + + + +
+ +
+
+ Administration secured by password: +
+ +
+
+
+
+ + diff --git a/isso/utils/__init__.py b/isso/utils/__init__.py index a7cafd5..7536041 100644 --- a/isso/utils/__init__.py +++ b/isso/utils/__init__.py @@ -5,9 +5,12 @@ from __future__ import division, unicode_literals import pkg_resources werkzeug = pkg_resources.get_distribution("werkzeug") -import json import hashlib +import json +import os +from datetime import datetime +from jinja2 import Environment, FileSystemLoader from werkzeug.wrappers import Response from werkzeug.exceptions import BadRequest @@ -109,6 +112,19 @@ class JSONRequest(Request): raise BadRequest('Unable to read JSON request') +def render_template(template_name, **context): + template_path = os.path.join(os.path.dirname(__file__), + '..', 'templates') + jinja_env = Environment(loader=FileSystemLoader(template_path), + autoescape=True) + def datetimeformat(value): + return datetime.fromtimestamp(value).strftime('%H:%M / %d-%m-%Y') + + jinja_env.filters['datetimeformat'] = datetimeformat + t = jinja_env.get_template(template_name) + return Response(t.render(context), mimetype='text/html') + + class JSONResponse(Response): def __init__(self, obj, *args, **kwargs): diff --git a/isso/views/comments.py b/isso/views/comments.py index d328305..68c228f 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -7,6 +7,7 @@ import cgi import time import functools +from datetime import datetime, timedelta from itsdangerous import SignatureExpired, BadSignature from werkzeug.http import dump_cookie @@ -15,11 +16,13 @@ from werkzeug.utils import redirect from werkzeug.routing import Rule from werkzeug.wrappers import Response from werkzeug.exceptions import BadRequest, Forbidden, NotFound +from werkzeug.contrib.securecookie import SecureCookie from isso.compat import text_type as str from isso import utils, local -from isso.utils import http, parse, JSONResponse as JSON +from isso.utils import (http, parse, JSONResponse as JSON, + render_template) from isso.views import requires from isso.utils.hash import sha1 @@ -90,7 +93,9 @@ class API(object): ('like', ('POST', '/id//like')), ('dislike', ('POST', '/id//dislike')), ('demo', ('GET', '/demo')), - ('preview', ('POST', '/preview')) + ('preview', ('POST', '/preview')), + ('login', ('POST', '/login')), + ('admin', ('GET', '/admin')) ] def __init__(self, isso, hasher): @@ -490,3 +495,41 @@ class API(object): def demo(self, env, req): return redirect(get_current_url(env) + '/index.html') + + def login(self, env, req): + data = req.form + password = self.isso.conf.get("general", "admin_password") + if data['password'] and data['password'] == password: + response = redirect(get_current_url(env, host_only=True) + '/admin') + cookie = functools.partial(dump_cookie, + value=self.isso.sign({"logged": True}), + expires=datetime.now() + timedelta(1)) + response.headers.add("Set-Cookie", cookie("admin-session")) + response.headers.add("X-Set-Cookie", cookie("isso-admin-session")) + return response + else: + return render_template('login.html') + + def admin(self, env, req): + try: + data = self.isso.unsign(req.cookies.get('admin-session', ''), + max_age=60 * 60 * 24) + except BadSignature: + return render_template('login.html') + if not data or not data['logged']: + return render_template('login.html') + page_size = 100 + page = req.args.get('page', 0) + mode = req.args.get('mode', 2) + comments = self.comments.fetchall(mode=mode, page=page, + limit=page_size) + comments_enriched = [] + for comment in list(comments): + comment['hash'] = self.isso.sign(comment['id']) + comments_enriched.append(comment) + comment_mode_count = self.comments.count_modes() + max_page = int(sum(comment_mode_count.values()) / 100) + return render_template('admin.html', comments=comments_enriched, + page=int(page), mode=int(mode), + conf=self.conf, max_page=max_page, + counts=comment_mode_count) diff --git a/setup.py b/setup.py index e54c110..b2c7482 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ import sys from setuptools import setup, find_packages -requires = ['itsdangerous', 'misaka>=1.0,<2.0', 'html5lib==0.9999999'] +requires = ['itsdangerous', 'misaka>=1.0,<2.0', 'html5lib==0.9999999', 'Jinja2'] if (3, 0) <= sys.version_info < (3, 3): raise SystemExit("Python 3.0, 3.1 and 3.2 are not supported") diff --git a/share/isso-dev.conf b/share/isso-dev.conf index ce87ab3..970c1b0 100644 --- a/share/isso-dev.conf +++ b/share/isso-dev.conf @@ -10,6 +10,7 @@ host = http://isso-dev.local/ max-age = 15m notify = stdout log-file = /var/log/isso.log +admin_password = strong_default_password_for_isso_admin [moderation] enabled = false diff --git a/share/isso.conf b/share/isso.conf index 7c275ac..5dcf987 100644 --- a/share/isso.conf +++ b/share/isso.conf @@ -43,9 +43,12 @@ max-age = 15m # moderated) and deletion links. notify = stdout -# Log console messages to file instead of standard out. +# Log console messages to file instead of standard output. log-file = +# Admin access password +admin_password = please_choose_a_strong_password + [moderation] # enable comment moderation queue. This option only affects new comments. From 7a79746f73ee928ea80e28a4657e8c5a6762920d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Sat, 16 Jul 2016 00:05:33 +0200 Subject: [PATCH 20/78] add: show author email --- isso/templates/admin.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/isso/templates/admin.html b/isso/templates/admin.html index f3d9a53..c32ff1e 100644 --- a/isso/templates/admin.html +++ b/isso/templates/admin.html @@ -108,6 +108,9 @@ function delete_com(com_id, hash) { {% else %} Anonymous {% endif %} + {% if comment.email %} + + {% endif %} {% if comment.website %} ({{comment.website}}) {% endif %} From 1516f56cbda9616cba2f6b8116c990574247d688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Sat, 16 Jul 2016 00:09:23 +0200 Subject: [PATCH 21/78] fix: cursor pointer on links delete/validate --- isso/css/admin.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/isso/css/admin.css b/isso/css/admin.css index 6f747b9..d6a8f74 100644 --- a/isso/css/admin.css +++ b/isso/css/admin.css @@ -113,3 +113,6 @@ a { text-align: center; width: 100%; } +.isso-comment-footer a { + cursor: pointer; +} From 0b6a0e4d5fc17be9280c80eef5f38c68c809ea5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Sat, 16 Jul 2016 00:53:52 +0200 Subject: [PATCH 22/78] add: group by thread --- isso/css/admin.css | 7 +++++++ isso/db/comments.py | 4 ++-- isso/templates/admin.html | 13 +++++++++++++ isso/views/comments.py | 7 +++++-- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/isso/css/admin.css b/isso/css/admin.css index d6a8f74..5a434fc 100644 --- a/isso/css/admin.css +++ b/isso/css/admin.css @@ -116,3 +116,10 @@ a { .isso-comment-footer a { cursor: pointer; } +.thread-title { + margin-left: 3em; +} +.group { + float: left; + margin-left: 2em; +} diff --git a/isso/db/comments.py b/isso/db/comments.py index 69790ac..62cfad2 100644 --- a/isso/db/comments.py +++ b/isso/db/comments.py @@ -137,10 +137,10 @@ class Comments: sql_args.append(parent) # custom sanitization - if order_by not in ['id', 'created', 'modified', 'likes', 'dislikes']: + if order_by not in ['id', 'created', 'modified', 'likes', 'dislikes', 'tid']: order_by = 'id' sql.append('ORDER BY ') - sql.append('comments.' + order_by) + sql.append('comments.' + order_by + ", comments.created") sql.append(' DESC') if limit: diff --git a/isso/templates/admin.html b/isso/templates/admin.html index c32ff1e..9d10816 100644 --- a/isso/templates/admin.html +++ b/isso/templates/admin.html @@ -77,6 +77,9 @@ function delete_com(com_id, hash) { +
+ Group by thread: +
+ {% set thread_id = "no_id" %} {% for comment in comments %} + {% if order_by == "tid" %} + {% if thread_id != comment.tid %} + {% set thread_id = comment.tid %} +

{{comment.title}}

+ {% endif %} + {% endif %}
{% if conf.avatar %}
@@ -103,6 +113,9 @@ function delete_com(com_id, hash) { {% endif %}
+ {% if order_by != "tid" %} + Thread: {{comment.title}}
+ {% endif %} {% if comment.author %} {{comment.author}} {% else %} diff --git a/isso/views/comments.py b/isso/views/comments.py index 68c228f..df1eebe 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -520,9 +520,11 @@ class API(object): return render_template('login.html') page_size = 100 page = req.args.get('page', 0) + order_by = req.args.get('order_by', "id") mode = req.args.get('mode', 2) comments = self.comments.fetchall(mode=mode, page=page, - limit=page_size) + limit=page_size, + order_by=order_by) comments_enriched = [] for comment in list(comments): comment['hash'] = self.isso.sign(comment['id']) @@ -532,4 +534,5 @@ class API(object): return render_template('admin.html', comments=comments_enriched, page=int(page), mode=int(mode), conf=self.conf, max_page=max_page, - counts=comment_mode_count) + counts=comment_mode_count, + order_by=order_by) From 3212bf762fc8fa152fd3323b0afaed103f68d728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Thu, 27 Oct 2016 23:18:37 +0200 Subject: [PATCH 23/78] fix 500 error on pagination --- isso/views/comments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isso/views/comments.py b/isso/views/comments.py index df1eebe..7dfd423 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -519,9 +519,9 @@ class API(object): if not data or not data['logged']: return render_template('login.html') page_size = 100 - page = req.args.get('page', 0) + page = int(req.args.get('page', 0)) order_by = req.args.get('order_by', "id") - mode = req.args.get('mode', 2) + mode = int(req.args.get('mode', 2)) comments = self.comments.fetchall(mode=mode, page=page, limit=page_size, order_by=order_by) From e3fddf4ae8e7de83323f4b535c7cd586b430efed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Sun, 13 Nov 2016 23:20:47 +0100 Subject: [PATCH 24/78] add: orders in administration --- isso/db/comments.py | 16 +++++++++++----- isso/templates/admin.html | 22 +++++++++++++++++++--- isso/views/comments.py | 8 +++++--- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/isso/db/comments.py b/isso/db/comments.py index 62cfad2..a66c248 100644 --- a/isso/db/comments.py +++ b/isso/db/comments.py @@ -110,7 +110,7 @@ class Comments: return dict(comment_count) def fetchall(self, mode=5, after=0, parent='any', order_by='id', - limit=100, page=0): + limit=100, page=0, asc=1): """ Return comments for admin with :param:`mode`. """ @@ -138,10 +138,16 @@ class Comments: # custom sanitization if order_by not in ['id', 'created', 'modified', 'likes', 'dislikes', 'tid']: - order_by = 'id' - sql.append('ORDER BY ') - sql.append('comments.' + order_by + ", comments.created") - sql.append(' DESC') + sql.append('ORDER BY ') + sql.append("comments.created") + if not asc: + sql.append(' DESC') + else: + sql.append('ORDER BY ') + sql.append('comments.' + order_by) + if not asc: + sql.append(' DESC') + sql.append(", comments.created") if limit: sql.append('LIMIT ?,?') diff --git a/isso/templates/admin.html b/isso/templates/admin.html index 9d10816..376aa59 100644 --- a/isso/templates/admin.html +++ b/isso/templates/admin.html @@ -61,17 +61,17 @@ function delete_com(com_id, hash) {
{% set thread_id = "no_id" %} diff --git a/isso/views/comments.py b/isso/views/comments.py index 7dfd423..9621d7d 100644 --- a/isso/views/comments.py +++ b/isso/views/comments.py @@ -520,11 +520,13 @@ class API(object): return render_template('login.html') page_size = 100 page = int(req.args.get('page', 0)) - order_by = req.args.get('order_by', "id") + order_by = req.args.get('order_by', None) + asc = int(req.args.get('asc', 1)) mode = int(req.args.get('mode', 2)) comments = self.comments.fetchall(mode=mode, page=page, limit=page_size, - order_by=order_by) + order_by=order_by, + asc=asc) comments_enriched = [] for comment in list(comments): comment['hash'] = self.isso.sign(comment['id']) @@ -535,4 +537,4 @@ class API(object): page=int(page), mode=int(mode), conf=self.conf, max_page=max_page, counts=comment_mode_count, - order_by=order_by) + order_by=order_by, asc=asc) From 2adb779fefa8f0aae10abc7e0da9ecb761088eab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Latinier?= Date: Tue, 6 Dec 2016 23:03:27 +0100 Subject: [PATCH 25/78] add: edit author/email/website/message --- isso/css/admin.css | 9 +++++ isso/templates/admin.html | 79 ++++++++++++++++++++++++++++++++++++--- isso/views/comments.py | 17 ++++++--- 3 files changed, 94 insertions(+), 11 deletions(-) diff --git a/isso/css/admin.css b/isso/css/admin.css index 5a434fc..c440528 100644 --- a/isso/css/admin.css +++ b/isso/css/admin.css @@ -123,3 +123,12 @@ a { float: left; margin-left: 2em; } +.editable { + border: 1px solid #aaa; + border-radius: 5px; + margin: 10px; + padding: 5px; +} +.hidden { + display: none; +} diff --git a/isso/templates/admin.html b/isso/templates/admin.html index 376aa59..12ac60c 100644 --- a/isso/templates/admin.html +++ b/isso/templates/admin.html @@ -39,12 +39,72 @@ function moderate(com_id, hash, action) { fade(document.getElementById("isso-" + com_id)); }}); } +function edit(com_id, hash, author, email, website, comment) { + ajax({method: "POST", + url: "/id/" + com_id + "/edit/" + hash, + data: JSON.stringify({text: comment, + author: author, + email: email, + website: website}), + success: function(ret){ + console.log("edit successed: ", ret);// TODO display some pretty stuff & update msg + }, + error: function(ret){ + console.log("Error: ", ret); // TODO flash msg/notif + }}); +} function validate_com(com_id, hash) { moderate(com_id, hash, "validate"); } function delete_com(com_id, hash) { moderate(com_id, hash, "delete"); } +function unset_editable(elt_id) { + var elt = document.getElementById(elt_id); + if (elt) { + elt.contentEditable = false; + elt.classList.remove("editable"); + } +} +function set_editable(elt_id) { + var elt = document.getElementById(elt_id); + if (elt) { + elt.contentEditable = true; + elt.classList.add("editable"); + } +} +function start_edit(com_id) { + var editable_elements = ['isso-author-' + com_id, + 'isso-email-' + com_id, + 'isso-website-' + com_id, + 'isso-text-' + com_id]; + for (var idx=0; idx <= editable_elements.length; idx++) { + set_editable(editable_elements[idx]); + } + document.getElementById('edit-btn-' + com_id).classList.toggle('hidden'); + document.getElementById('stop-edit-btn-' + com_id).classList.toggle('hidden'); + document.getElementById('send-edit-btn-' + com_id).classList.toggle('hidden'); +} +function stop_edit(com_id) { + var editable_elements = ['isso-author-' + com_id, + 'isso-email-' + com_id, + 'isso-website-' + com_id, + 'isso-text-' + com_id]; + for (var idx=0; idx <= editable_elements.length; idx++) { + unset_editable(editable_elements[idx]); + } + document.getElementById('edit-btn-' + com_id).classList.toggle('hidden'); + document.getElementById('stop-edit-btn-' + com_id).classList.toggle('hidden'); + document.getElementById('send-edit-btn-' + com_id).classList.toggle('hidden'); +} +function send_edit(com_id, hash) { + var author = document.getElementById('isso-author-' + com_id).textContent; + var email = document.getElementById('isso-email-' + com_id).textContent; + var website = document.getElementById('isso-website-' + com_id).textContent; + var comment = document.getElementById('isso-text-' + com_id).textContent; + edit(com_id, hash, author, email, website, comment); + stop_edit(com_id); +}
@@ -130,18 +190,22 @@ function delete_com(com_id, hash) {
{% if order_by != "tid" %} - Thread: {{comment.title}}
+
Thread: {{comment.title}}

{% endif %} {% if comment.author %} - {{comment.author}} + {{comment.author}} {% else %} - Anonymous + Anonymous {% endif %} {% if comment.email %} - + ({{comment.email}} ) + {% else %} + {% endif %} {% if comment.website %} - ({{comment.website}}) + ({{comment.website}} open) + {% else %} + {% endif %} @@ -159,13 +223,16 @@ function delete_com(com_id, hash) { {% if comment.mode == 4 %} HIDDEN. Original text:
{% endif %} - {{comment.text}} +
{{comment.text}}