Merge remote-tracking branch 'posativ/master' into reply_notification
Conflicts: isso/css/isso.css isso/ext/notifications.py isso/js/app/text/postbox.jade isso/views/comments.py
This commit is contained in:
commit
f6f61c547a
13
CHANGES.rst
13
CHANGES.rst
@ -4,7 +4,8 @@ Changelog for Isso
|
||||
0.10.7 (unreleased)
|
||||
-------------------
|
||||
|
||||
- Fix Chinese translation
|
||||
- Fix Chinese translation & typo in CJK
|
||||
- Fix link in moderation mails if isso is setup on a sub-url (e.g. domain.tld/comments/)
|
||||
- Add Danish translation
|
||||
- Add Hungarian translation
|
||||
- Add Persian translation
|
||||
@ -12,7 +13,15 @@ Changelog for Isso
|
||||
- Add links highlighting in comments
|
||||
- Add apidoc
|
||||
- Add rc.d script for FreeBSD
|
||||
- Some tests/travis/documentation improvements and fixes
|
||||
- Add the possibility to set CORS Origin through ISSO_CORS_ORIGIN environ variable
|
||||
- Add preview button
|
||||
- Add Atom feed at /feed?uri={thread-id}
|
||||
- Add optionnal gravatar support
|
||||
- Add nofollow noopener on links inside comments
|
||||
- Add Dockerfile
|
||||
- Upgraded to Misaka 2
|
||||
- Some tests/travis/documentation improvements and fixes + pep8
|
||||
- Improvement on german translation
|
||||
|
||||
|
||||
0.10.6 (2016-09-22)
|
||||
|
@ -51,12 +51,46 @@ In chronological order:
|
||||
* Added configuration to require email addresses (no validation)
|
||||
* Fix Vagrantfile
|
||||
|
||||
* Benoît Latinier <benoit@latinier.fr>
|
||||
* Benoît Latinier @blatinier <benoit@latinier.fr>
|
||||
* Fix thread discovery
|
||||
* Added mandatory author
|
||||
* Added admin interface
|
||||
|
||||
* Ivan Pantic <ivanpantic82@gmail.com>
|
||||
* Added vote levels
|
||||
|
||||
* Martin Schenck @schemar
|
||||
* Improvement in the german translation
|
||||
|
||||
* @cclauss
|
||||
* Pep8 and drop of legacy supports (old python & debian version tested in travis)
|
||||
* Make travis use pyflakes
|
||||
|
||||
* Lucas Cimon @Lucas-C
|
||||
* Added the possibility to define CORS origins through ISSO_CORS_ORIGIN environment variable
|
||||
* Fix a bug with <a> in <svg>
|
||||
|
||||
* Yuchen Pei @ycpei
|
||||
* Fix link in moderation emails when isso is installed in a sub URL
|
||||
|
||||
* @Rocket1184
|
||||
* Fix typo in CJK translations
|
||||
|
||||
* @vincentbernat
|
||||
* Added documentation about data-isso-id attribute (overriding the standard isso-thread-id)
|
||||
* Added multi-staged Dockerfile
|
||||
* Added atom feed
|
||||
* Added a nofollow/noopener on links inside comments to protect against bots
|
||||
* Added a preview using the existing preview endpoint
|
||||
|
||||
* @p-vitt & @M4a1x
|
||||
* Documentation on troubleshooting for uberspace users
|
||||
|
||||
* @benjhess
|
||||
* Optionnal gravatar support
|
||||
|
||||
* Steffen Prince @sprin
|
||||
* Upgrade to Misaka 2
|
||||
|
||||
* [Your name or handle] <[email or website]>
|
||||
* [Brief summary of your changes]
|
||||
|
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@ -0,0 +1,33 @@
|
||||
# First, compile JS stuff
|
||||
FROM node
|
||||
WORKDIR /src/
|
||||
COPY . .
|
||||
RUN npm install -g requirejs uglify-js jade bower
|
||||
RUN make init js
|
||||
|
||||
# Second, create virtualenv
|
||||
FROM python:3-stretch
|
||||
WORKDIR /src/
|
||||
COPY --from=0 /src .
|
||||
RUN apt-get -qqy update && apt-get -qqy install python3-dev sqlite3
|
||||
RUN python3 -m venv /isso \
|
||||
&& . /isso/bin/activate \
|
||||
&& python setup.py install \
|
||||
&& pip install gunicorn
|
||||
|
||||
# Third, create final repository
|
||||
FROM python:3-slim-stretch
|
||||
WORKDIR /isso/
|
||||
COPY --from=1 /isso .
|
||||
|
||||
# Configuration
|
||||
VOLUME /db /config
|
||||
EXPOSE 8080
|
||||
ENV ISSO_SETTINGS /config/isso.cfg
|
||||
CMD ["/isso/bin/gunicorn", "-b", "0.0.0.0:8080", "-w", "4", "--preload", "isso.run"]
|
||||
|
||||
# Example of use:
|
||||
#
|
||||
# docker build -t isso .
|
||||
# docker run -it --rm -v /opt/isso:/config -v /opt/isso:/db -v $PWD:$PWD isso /isso/bin/isso -c \$ISSO_SETTINGS import disqus.xml
|
||||
# docker run -d --rm --name isso -p 8080:8080 -v /opt/isso:/config -v /opt/isso:/db isso
|
@ -59,5 +59,3 @@ definitely need help:
|
||||
- delete or activate comments matching a filter (e.g. name, email, ip address)
|
||||
|
||||
- close threads and remove threads completely
|
||||
|
||||
- edit comments
|
||||
|
@ -7,6 +7,7 @@ preferably in the script tag which embeds the JS:
|
||||
.. code-block:: html
|
||||
|
||||
<script data-isso="/prefix/"
|
||||
data-isso-id="thread-id"
|
||||
data-isso-css="true"
|
||||
data-isso-lang="ru"
|
||||
data-isso-reply-to-self="false"
|
||||
@ -19,15 +20,16 @@ preferably in the script tag which embeds the JS:
|
||||
data-isso-avatar-bg="#f0f0f0"
|
||||
data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."
|
||||
data-isso-vote="true"
|
||||
data-vote-levels=""
|
||||
data-isso-vote-levels=""
|
||||
data-isso-feed="false"
|
||||
src="/prefix/js/embed.js"></script>
|
||||
|
||||
Furthermore you can override the automatic title detection inside
|
||||
the embed tag, e.g.:
|
||||
the embed tag, as well as the thread ID, e.g.:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<section id="isso-thread" data-title="Foo!"></section>
|
||||
<section id="isso-thread" data-title="Foo!" data-isso-id="/path/to/resource"></section>
|
||||
|
||||
data-isso
|
||||
---------
|
||||
@ -106,6 +108,14 @@ scheme is based in `this color palette <http://colrd.com/palette/19308/>`_.
|
||||
Multiple colors must be separated by space. If you use less than eight colors
|
||||
and not a multiple of 2, the color distribution is not even.
|
||||
|
||||
data-isso-gravatar
|
||||
------------------
|
||||
|
||||
Uses gravatar images instead of generating svg images. You have to set
|
||||
"data-isso-avatar" to **false** when you want to use this. Otherwise
|
||||
both the gravatar and avatar svg image will show up. Please also set
|
||||
option "gravatar" to **true** in the server configuration...
|
||||
|
||||
data-isso-vote
|
||||
--------------
|
||||
|
||||
@ -124,3 +134,10 @@ For example, the value `"-5,5"` will cause each `isso-comment` to be given one o
|
||||
- `isso-vote-level-2` for scores of `5` and greater
|
||||
|
||||
These classes can then be used to customize the appearance of comments (eg. put a star on popular comments)
|
||||
|
||||
data-isso-feed
|
||||
--------------
|
||||
|
||||
Enable or disable the addition of a link to the feed for the comment
|
||||
thread. The link will only be valid if the appropriate setting, in
|
||||
``[rss]`` section, is also enabled server-side.
|
||||
|
@ -91,6 +91,18 @@ notify
|
||||
log-file
|
||||
Log console messages to file instead of standard out.
|
||||
|
||||
gravatar
|
||||
When set to ``true`` this will add the property "gravatar_image"
|
||||
containing the link to a gravatar image to every comment. If a comment
|
||||
does not contain an email address, gravatar will render a random icon.
|
||||
This is only true when using the default value for "gravatar-url"
|
||||
which contains the query string param ``d=identicon`` ...
|
||||
|
||||
gravatar-url
|
||||
Url for gravatar images. The "{}" is where the email hash will be placed.
|
||||
Defaults to "https://www.gravatar.com/avatar/{}?d=identicon"
|
||||
|
||||
|
||||
|
||||
.. _CORS: https://developer.mozilla.org/en/docs/HTTP/Access_control_CORS
|
||||
|
||||
@ -108,7 +120,7 @@ Enable moderation queue and handling of comments still in moderation queue
|
||||
|
||||
enabled
|
||||
enable comment moderation queue. This option only affects new comments.
|
||||
Comments in modertion queue are not visible to other users until you
|
||||
Comments in moderation queue are not visible to other users until you
|
||||
activate them.
|
||||
|
||||
purge-after
|
||||
@ -308,6 +320,27 @@ algorithm
|
||||
Arguments have to be in that order, but can be reduced to `pbkdf2:4096`
|
||||
for example to override the iterations only.
|
||||
|
||||
.. _configure-rss:
|
||||
|
||||
RSS
|
||||
---
|
||||
|
||||
Isso can provide an Atom feed for each comment thread. Users can use
|
||||
them to subscribe to comments and be notified of changes. Atom feeds
|
||||
are enabled as soon as there is a base URL defined in this section.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[rss]
|
||||
base =
|
||||
limit = 100
|
||||
|
||||
base
|
||||
base URL to use to build complete URI to pages (by appending the URI from Isso)
|
||||
|
||||
limit
|
||||
number of most recent comments to return for a thread
|
||||
|
||||
Appendum
|
||||
--------
|
||||
|
||||
|
@ -185,3 +185,16 @@ uri :
|
||||
|
||||
returns an integer
|
||||
|
||||
Get Atom feed
|
||||
-------------
|
||||
|
||||
Get an Atom feed of comments for thread `uri`:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
GET /feed?uri=%2Fhello-world%2F
|
||||
|
||||
uri :
|
||||
URI to get comments for, required.
|
||||
|
||||
Returns an XML document as the Atom feed.
|
||||
|
@ -39,10 +39,10 @@ package manager.
|
||||
.. code-block:: sh
|
||||
|
||||
# for Debian/Ubuntu
|
||||
~> sudo apt-get install python-setuptools python-virtualenv
|
||||
~> sudo apt-get install python-setuptools python-virtualenv python-dev
|
||||
|
||||
# Fedora/Red Hat
|
||||
~> sudo yum install python-setuptools python-virtualenv
|
||||
~> sudo yum install python-setuptools python-virtualenv python-devel
|
||||
|
||||
The next steps should be done as regular user, not as root (although possible
|
||||
but not recommended):
|
||||
@ -149,7 +149,18 @@ Prebuilt Packages
|
||||
* Fedora: https://copr.fedoraproject.org/coprs/jujens/isso/ — copr
|
||||
repository. Built from Pypi, includes a systemctl unit script.
|
||||
|
||||
* Docker Image: https://registry.hub.docker.com/u/bl4n/isso/
|
||||
Build a Docker image
|
||||
--------------------
|
||||
|
||||
You can get a Docker image by running ``docker build . -t
|
||||
isso``. Assuming you have your configuration in ``/opt/isso``, you can
|
||||
use the following command to spawn the Docker container:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
~> docker run -d --rm --name isso -p 127.0.0.1:8080:8080 -v /opt/isso:/config -v /opt/isso:/db isso
|
||||
|
||||
Then, you can use a reverse proxy to expose port 8080.
|
||||
|
||||
Install from Source
|
||||
-------------------
|
||||
|
@ -1,6 +1,12 @@
|
||||
Troubleshooting
|
||||
===============
|
||||
|
||||
For uberspace users
|
||||
-------------------
|
||||
Some uberspace users experienced problems with isso and they solved their
|
||||
issue by adding `DirectoryIndex disabled` as the first line in the `.htaccess`
|
||||
file for the domain the isso server is running on.
|
||||
|
||||
pkg_ressources.DistributionNotFound
|
||||
-----------------------------------
|
||||
|
||||
|
@ -15,6 +15,14 @@
|
||||
color: #555;
|
||||
font-weight: bold;
|
||||
}
|
||||
#isso-thread > .isso-feedlink {
|
||||
float: right;
|
||||
padding-left: 1em;
|
||||
}
|
||||
#isso-thread > .isso-feedlink > a {
|
||||
font-size: 0.8em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
#isso-thread .textarea {
|
||||
min-height: 58px;
|
||||
outline: 0;
|
||||
@ -23,31 +31,26 @@
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.isso-comment {
|
||||
#isso-root .isso-comment {
|
||||
max-width: 68em;
|
||||
padding-top: 0.95em;
|
||||
margin: 0.95em auto;
|
||||
}
|
||||
.isso-comment:not(:first-of-type),
|
||||
#isso-root .preview .isso-comment {
|
||||
padding-top: 0;
|
||||
margin: 0;
|
||||
}
|
||||
#isso-root .isso-comment:not(:first-of-type),
|
||||
.isso-follow-up .isso-comment {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.isso-comment > div.avatar,
|
||||
.isso-postbox > .avatar {
|
||||
.isso-comment > div.avatar {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 7%;
|
||||
margin: 3px 15px 0 0;
|
||||
}
|
||||
.isso-postbox > .avatar {
|
||||
float: left;
|
||||
margin: 5px 10px 0 5px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.isso-comment > div.avatar > svg,
|
||||
.isso-postbox > .avatar > svg {
|
||||
.isso-comment > div.avatar > svg {
|
||||
max-width: 48px;
|
||||
max-height: 48px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
@ -90,7 +93,8 @@
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
.isso-comment > div.text-wrapper > .textarea-wrapper .textarea {
|
||||
.isso-comment > div.text-wrapper > .textarea-wrapper .textarea,
|
||||
.isso-comment > div.text-wrapper > .textarea-wrapper .preview {
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
.isso-comment > div.text-wrapper > div.text p {
|
||||
@ -108,7 +112,8 @@
|
||||
font-size: 130%;
|
||||
font-weight: bold;
|
||||
}
|
||||
.isso-comment > div.text-wrapper > div.textarea-wrapper .textarea {
|
||||
.isso-comment > div.text-wrapper > div.textarea-wrapper .textarea,
|
||||
.isso-comment > div.text-wrapper > div.textarea-wrapper .preview {
|
||||
width: 100%;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 2px;
|
||||
@ -119,10 +124,12 @@
|
||||
color: gray !important;
|
||||
clear: left;
|
||||
}
|
||||
.isso-feedlink,
|
||||
.isso-comment > div.text-wrapper > .isso-comment-footer a {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
.isso-feedlink:hover,
|
||||
.isso-comment > div.text-wrapper > .isso-comment-footer a:hover {
|
||||
color: #111111 !important;
|
||||
text-shadow: #aaaaaa 0 0 1px !important;
|
||||
@ -152,6 +159,7 @@
|
||||
.isso-postbox {
|
||||
max-width: 68em;
|
||||
margin: 0 auto 2em;
|
||||
clear: right;
|
||||
}
|
||||
.isso-postbox > .form-wrapper {
|
||||
display: block;
|
||||
@ -161,7 +169,8 @@
|
||||
.isso-postbox > .form-wrapper > .auth-section .post-action {
|
||||
display: block;
|
||||
}
|
||||
.isso-postbox > .form-wrapper .textarea {
|
||||
.isso-postbox > .form-wrapper .textarea,
|
||||
.isso-postbox > .form-wrapper .preview {
|
||||
margin: 0 0 .3em;
|
||||
padding: .4em .8em;
|
||||
border-radius: 3px;
|
||||
@ -191,7 +200,7 @@
|
||||
.isso-postbox > .form-wrapper > .auth-section .post-action {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin: 0;
|
||||
margin: 0 0 0 5px;
|
||||
}
|
||||
.isso-postbox > .form-wrapper > .auth-section .post-action > input {
|
||||
padding: calc(.3em - 1px);
|
||||
@ -209,6 +218,27 @@
|
||||
.isso-postbox > .form-wrapper > .auth-section .post-action > input:active {
|
||||
background-color: #BBB;
|
||||
}
|
||||
.isso-postbox > .form-wrapper .preview,
|
||||
.isso-postbox > .form-wrapper input[name="edit"],
|
||||
.isso-postbox.preview-mode > .form-wrapper input[name="preview"],
|
||||
.isso-postbox.preview-mode > .form-wrapper .textarea {
|
||||
display: none;
|
||||
}
|
||||
.isso-postbox.preview-mode > .form-wrapper .preview {
|
||||
display: block;
|
||||
}
|
||||
.isso-postbox.preview-mode > .form-wrapper input[name="edit"] {
|
||||
display: inline;
|
||||
}
|
||||
.isso-postbox > .form-wrapper .preview {
|
||||
background-color: #f8f8f8;
|
||||
background: repeating-linear-gradient(
|
||||
-45deg,
|
||||
#f8f8f8,
|
||||
#f8f8f8 10px,
|
||||
#fff 10px,
|
||||
#fff 20px
|
||||
);
|
||||
.isso-postbox > .form-wrapper > .notification-section {
|
||||
display: none;
|
||||
padding-bottom: 10px;
|
||||
@ -222,9 +252,4 @@
|
||||
.isso-postbox > .form-wrapper > .auth-section .input-wrapper input {
|
||||
width: 100%;
|
||||
}
|
||||
.isso-postbox > .form-wrapper > .auth-section .post-action {
|
||||
display: block;
|
||||
float: none;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
@ -173,7 +173,8 @@ class Comments:
|
||||
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):
|
||||
def fetch(self, uri, mode=5, after=0, parent='any',
|
||||
order_by='id', asc=1, limit=None):
|
||||
"""
|
||||
Return comments for :param:`uri` with :param:`mode`.
|
||||
"""
|
||||
@ -195,7 +196,8 @@ class Comments:
|
||||
order_by = 'id'
|
||||
sql.append('ORDER BY ')
|
||||
sql.append(order_by)
|
||||
sql.append(' ASC')
|
||||
if not asc:
|
||||
sql.append(' DESC')
|
||||
|
||||
if limit:
|
||||
sql.append('LIMIT ?')
|
||||
|
@ -70,6 +70,12 @@ class SMTP(object):
|
||||
|
||||
self.isso = isso
|
||||
self.conf = isso.conf.section("smtp")
|
||||
gh = isso.conf.get("general", "host")
|
||||
if type(gh) == str:
|
||||
self.general_host = gh
|
||||
#if gh is not a string then gh is a list
|
||||
else:
|
||||
self.general_host = gh[0]
|
||||
|
||||
# test SMTP connectivity
|
||||
try:
|
||||
@ -118,7 +124,7 @@ class SMTP(object):
|
||||
rv.write("---\n")
|
||||
|
||||
if admin:
|
||||
uri = local("host") + "/id/%i" % comment["id"]
|
||||
uri = self.general_host + "/id/%i" % comment["id"]
|
||||
key = self.isso.sign(comment["id"])
|
||||
|
||||
rv.write("Delete comment: %s\n" % (uri + "/delete/" + key))
|
||||
@ -127,7 +133,7 @@ class SMTP(object):
|
||||
rv.write("Activate comment: %s\n" % (uri + "/activate/" + key))
|
||||
|
||||
else:
|
||||
uri = local("host") + "/id/%i" % comment_parent["id"]
|
||||
uri = self.general_host + "/id/%i" % comment_parent["id"]
|
||||
key = self.isso.sign(('unsubscribe', comment_parent["id"]))
|
||||
|
||||
rv.write("Unsubscribe from this conversation: %s\n" % (uri + "/unsubscribe/" + key))
|
||||
|
@ -191,6 +191,24 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) {
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
|
||||
var feed = function(tid) {
|
||||
return endpoint + "/feed?" + qs({uri: tid || location});
|
||||
};
|
||||
|
||||
var preview = function(text) {
|
||||
var deferred = Q.defer();
|
||||
curl("POST", endpoint + "/preview", JSON.stringify({text: text}),
|
||||
function(rv) {
|
||||
if (rv.status === 200) {
|
||||
deferred.resolve(JSON.parse(rv.body).text);
|
||||
} else {
|
||||
deferred.reject(rv.body);
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return {
|
||||
endpoint: endpoint,
|
||||
salt: salt,
|
||||
@ -202,6 +220,8 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) {
|
||||
fetch: fetch,
|
||||
count: count,
|
||||
like: like,
|
||||
dislike: dislike
|
||||
dislike: dislike,
|
||||
feed: feed,
|
||||
preview: preview
|
||||
};
|
||||
});
|
||||
|
@ -10,12 +10,14 @@ define(function() {
|
||||
"max-comments-top": "inf",
|
||||
"max-comments-nested": 5,
|
||||
"reveal-on-click": 5,
|
||||
"gravatar": false,
|
||||
"avatar": true,
|
||||
"avatar-bg": "#f0f0f0",
|
||||
"avatar-fg": ["#9abf88", "#5698c4", "#e279a3", "#9163b6",
|
||||
"#be5168", "#f19670", "#e4bf80", "#447c69"].join(" "),
|
||||
"vote": true,
|
||||
"vote-levels": null
|
||||
"vote-levels": null,
|
||||
"feed": false
|
||||
};
|
||||
|
||||
var js = document.getElementsByTagName("script");
|
||||
|
@ -4,7 +4,7 @@ define(["app/api", "app/dom", "app/i18n"], function(api, $, i18n) {
|
||||
var objs = {};
|
||||
|
||||
$.each("a", function(el) {
|
||||
if (! el.href.match(/#isso-thread$/)) {
|
||||
if (! el.href.match || ! el.href.match(/#isso-thread$/)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Име/псевдоним (незадължително)",
|
||||
"postbox-email": "Ел. поща (незадължително)",
|
||||
"postbox-website": "Уебсайт (незадължително)",
|
||||
"postbox-preview": "преглед",
|
||||
"postbox-edit": "Редактиране",
|
||||
"postbox-submit": "Публикуване",
|
||||
"num-comments": "1 коментар\n{{ n }} коментара",
|
||||
"no-comments": "Все още няма коментари",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Jméno (nepovinné)",
|
||||
"postbox-email": "E-mail (nepovinný)",
|
||||
"postbox-website": "Web (nepovinný)",
|
||||
"postbox-preview": "Náhled",
|
||||
"postbox-edit": "Upravit",
|
||||
"postbox-submit": "Publikovat",
|
||||
"num-comments": "Jeden komentář\n{{ n }} Komentářů",
|
||||
"no-comments": "Zatím bez komentářů",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Name (optional)",
|
||||
"postbox-email": "E-mail (optional)",
|
||||
"postbox-website": "Website (optional)",
|
||||
"postbox-preview": "Eksempel",
|
||||
"postbox-edit": "Rediger",
|
||||
"postbox-submit": "Submit",
|
||||
|
||||
"num-comments": "One Comment\n{{ n }} Comments",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Name (optional)",
|
||||
"postbox-email": "Email (optional)",
|
||||
"postbox-website": "Website (optional)",
|
||||
"postbox-preview": "Vorschau",
|
||||
"postbox-edit": "Bearbeiten",
|
||||
"postbox-submit": "Abschicken",
|
||||
"num-comments": "1 Kommentar\n{{ n }} Kommentare",
|
||||
"no-comments": "Bisher keine Kommentare",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Όνομα (προαιρετικό)",
|
||||
"postbox-email": "E-mail (προαιρετικό)",
|
||||
"postbox-website": "Ιστοσελίδα (προαιρετικό)",
|
||||
"postbox-preview": "Πρεμιέρα",
|
||||
"postbox-edit": "Επεξεργασία",
|
||||
"postbox-submit": "Υποβολή",
|
||||
"num-comments": "Ένα σχόλιο\n{{ n }} σχόλια",
|
||||
"no-comments": "Δεν υπάρχουν σχόλια",
|
||||
|
@ -3,11 +3,14 @@ define({
|
||||
"postbox-author": "Name (optional)",
|
||||
"postbox-email": "E-mail (optional)",
|
||||
"postbox-website": "Website (optional)",
|
||||
"postbox-preview": "Preview",
|
||||
"postbox-edit": "Edit",
|
||||
"postbox-submit": "Submit",
|
||||
"postbox-notification": "Subscribe to email notification of replies",
|
||||
|
||||
"num-comments": "One Comment\n{{ n }} Comments",
|
||||
"no-comments": "No Comments Yet",
|
||||
"atom-feed": "Atom feed",
|
||||
|
||||
"comment-reply": "Reply",
|
||||
"comment-edit": "Edit",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Nomo (malnepra)",
|
||||
"postbox-email": "Retadreso (malnepra)",
|
||||
"postbox-website": "Retejo (malnepra)",
|
||||
"postbox-preview": "Antaŭrigardo",
|
||||
"postbox-edit": "Redaktu",
|
||||
"postbox-submit": "Sendu",
|
||||
"num-comments": "{{ n }} komento\n{{ n }} komentoj",
|
||||
"no-comments": "Neniu komento ankoraŭ",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Nombre (opcional)",
|
||||
"postbox-email": "E-mail (opcional)",
|
||||
"postbox-website": "Sitio web (opcional)",
|
||||
"postbox-preview": "Avance",
|
||||
"postbox-edit": "Editar",
|
||||
"postbox-submit": "Enviar",
|
||||
"num-comments": "Un Comentario\n{{ n }} Comentarios",
|
||||
"no-comments": "Sin Comentarios Todavía",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "اسم (اختیاری)",
|
||||
"postbox-email": "ایمیل (اختیاری)",
|
||||
"postbox-website": "سایت (اختیاری)",
|
||||
"postbox-preview": "پیشنمایش",
|
||||
"postbox-edit": "ویرایش",
|
||||
"postbox-submit": "ارسال",
|
||||
|
||||
"num-comments": "یک نظر\n{{ n }} نظر",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Nimi (valinnainen)",
|
||||
"postbox-email": "Sähköposti (valinnainen)",
|
||||
"postbox-website": "Web-sivu (valinnainen)",
|
||||
"postbox-preview": "Esikatselu",
|
||||
"postbox-edit": "Muokkaa",
|
||||
"postbox-submit": "Lähetä",
|
||||
|
||||
"num-comments": "Yksi kommentti\n{{ n }} kommenttia",
|
||||
|
@ -3,10 +3,13 @@ define({
|
||||
"postbox-author": "Nom (optionnel)",
|
||||
"postbox-email": "Courriel (optionnel)",
|
||||
"postbox-website": "Site web (optionnel)",
|
||||
"postbox-preview": "Aperçu",
|
||||
"postbox-edit": "Éditer",
|
||||
"postbox-submit": "Soumettre",
|
||||
"postbox-notification": "S'abonner aux notifications de réponses",
|
||||
"num-comments": "{{ n }} commentaire\n{{ n }} commentaires",
|
||||
"no-comments": "Aucun commentaire pour l'instant",
|
||||
"atom-feed": "Flux Atom",
|
||||
"comment-reply": "Répondre",
|
||||
"comment-edit": "Éditer",
|
||||
"comment-save": "Enregistrer",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Ime (neobavezno)",
|
||||
"postbox-email": "E-mail (neobavezno)",
|
||||
"postbox-website": "Web stranica (neobavezno)",
|
||||
"postbox-preview": "Pregled",
|
||||
"postbox-edit": "Uredi",
|
||||
"postbox-submit": "Pošalji",
|
||||
"num-comments": "Jedan komentar\n{{ n }} komentara",
|
||||
"no-comments": "Još nema komentara",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Név (nem kötelező)",
|
||||
"postbox-email": "Email (nem kötelező)",
|
||||
"postbox-website": "Website (nem kötelező)",
|
||||
"postbox-preview": "Előnézet",
|
||||
"postbox-edit": "Szerekesztés",
|
||||
"postbox-submit": "Elküld",
|
||||
"num-comments": "Egy hozzászólás\n{{ n }} hozzászólás",
|
||||
"no-comments": "Eddig nincs hozzászólás",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Nome (opzionale)",
|
||||
"postbox-email": "E-mail (opzionale)",
|
||||
"postbox-website": "Sito web (opzionale)",
|
||||
"postbox-preview": "Anteprima",
|
||||
"postbox-edit": "Modifica",
|
||||
"postbox-submit": "Invia",
|
||||
"num-comments": "Un Commento\n{{ n }} Commenti",
|
||||
"no-comments": "Ancora Nessun Commento",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Naam (optioneel)",
|
||||
"postbox-email": "E-mail (optioneel)",
|
||||
"postbox-website": "Website (optioneel)",
|
||||
"postbox-preview": "Voorbeeld",
|
||||
"postbox-edit": "Bewerken",
|
||||
"postbox-submit": "Versturen",
|
||||
"num-comments": "Één reactie\n{{ n }} reacties",
|
||||
"no-comments": "Nog geen reacties",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Imię/nick (opcjonalnie)",
|
||||
"postbox-email": "E-mail (opcjonalnie)",
|
||||
"postbox-website": "Strona (opcjonalnie)",
|
||||
"postbox-preview": "Visualizar",
|
||||
"postbox-edit": "Edytuj",
|
||||
"postbox-submit": "Wyślij",
|
||||
"num-comments": "Jeden komentarz\n{{ n }} komentarzy",
|
||||
"no-comments": "Jeszcze nie ma komentarzy",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Имя (необязательно)",
|
||||
"postbox-email": "Email (необязательно)",
|
||||
"postbox-website": "Сайт (необязательно)",
|
||||
"postbox-preview": "анонс",
|
||||
"postbox-edit": "Правка",
|
||||
"postbox-submit": "Отправить",
|
||||
"num-comments": "{{ n }} комментарий\n{{ n }} комментария\n{{ n }} комментариев",
|
||||
"no-comments": "Пока нет комментариев",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Namn (frivilligt)",
|
||||
"postbox-email": "E-mail (frivilligt)",
|
||||
"postbox-website": "Hemsida (frivilligt)",
|
||||
"postbox-preview": "Förhandsvisning",
|
||||
"postbox-edit": "Redigera",
|
||||
"postbox-submit": "Skicka",
|
||||
"num-comments": "En kommentar\n{{ n }} kommentarer",
|
||||
"no-comments": "Inga kommentarer än",
|
||||
|
@ -3,6 +3,8 @@ define({
|
||||
"postbox-author": "Tên (tùy chọn)",
|
||||
"postbox-email": "E-mail (tùy chọn)",
|
||||
"postbox-website": "Website (tùy chọn)",
|
||||
"postbox-preview": "Xem trước",
|
||||
"postbox-edit": "Sửa",
|
||||
"postbox-submit": "Gửi",
|
||||
|
||||
"num-comments": "Một bình luận\n{{ n }} bình luận",
|
||||
|
@ -1,11 +1,13 @@
|
||||
define({
|
||||
"postbox-text": "在此输入评论 (最少3个字符)",
|
||||
"postbox-text": "在此输入评论 (最少 3 个字符)",
|
||||
"postbox-author": "名字 (可选)",
|
||||
"postbox-email": "E-mail (可选)",
|
||||
"postbox-website": "网站 (可选)",
|
||||
"postbox-preview": "预习",
|
||||
"postbox-edit": "编辑",
|
||||
"postbox-submit": "提交",
|
||||
|
||||
"num-comments": "1条评论\n{{ n }}条评论",
|
||||
"num-comments": "1 条评论\n{{ n }} 条评论",
|
||||
"no-comments": "还没有评论",
|
||||
|
||||
"comment-reply": "回复",
|
||||
@ -21,10 +23,10 @@ define({
|
||||
"comment-hidden": "{{ n }} 条评论已隐藏",
|
||||
|
||||
"date-now": "刚刚",
|
||||
"date-minute": "1分钟前\n{{ n }}分钟前",
|
||||
"date-hour": "1小时前\n{{ n }}小时前",
|
||||
"date-day": "昨天\n{{ n }}天前",
|
||||
"date-week": "上周\n{{ n }}周前",
|
||||
"date-month": "上个月\n{{ n }}个月前",
|
||||
"date-year": "去年\n{{ n }}年前"
|
||||
"date-minute": "1 分钟前\n{{ n }} 分钟前",
|
||||
"date-hour": "1 小时前\n{{ n }} 小时前",
|
||||
"date-day": "昨天\n{{ n }} 天前",
|
||||
"date-week": "上周\n{{ n }} 周前",
|
||||
"date-month": "上个月\n{{ n }} 个月前",
|
||||
"date-year": "去年\n{{ n }} 年前"
|
||||
});
|
||||
|
@ -1,11 +1,13 @@
|
||||
define({
|
||||
"postbox-text": "在此輸入留言(至少3個字元)",
|
||||
"postbox-text": "在此輸入留言(至少 3 個字元)",
|
||||
"postbox-author": "名稱 (非必填)",
|
||||
"postbox-email": "電子信箱 (非必填)",
|
||||
"postbox-website": "個人網站 (非必填)",
|
||||
"postbox-preview": "預習",
|
||||
"postbox-edit": "編輯",
|
||||
"postbox-submit": "送出",
|
||||
|
||||
"num-comments": "1則留言\n{{ n }}則留言",
|
||||
"num-comments": "1 則留言\n{{ n }} 則留言",
|
||||
"no-comments": "尚無留言",
|
||||
|
||||
"comment-reply": "回覆",
|
||||
@ -18,13 +20,13 @@ define({
|
||||
"comment-deleted": "留言已刪",
|
||||
"comment-queued": "留言待審",
|
||||
"comment-anonymous": "匿名",
|
||||
"comment-hidden": "{{ n }}則隱藏留言",
|
||||
"comment-hidden": "{{ n }} 則隱藏留言",
|
||||
|
||||
"date-now": "剛剛",
|
||||
"date-minute": "1分鐘前\n{{ n }}分鐘前",
|
||||
"date-hour": "1小時前\n{{ n }}小時前",
|
||||
"date-day": "昨天\n{{ n }}天前",
|
||||
"date-week": "上週\n{{ n }}週前",
|
||||
"date-month": "上個月\n{{ n }}個月前",
|
||||
"date-year": "去年\n{{ n }}年前"
|
||||
"date-minute": "1 分鐘前\n{{ n }} 分鐘前",
|
||||
"date-hour": "1 小時前\n{{ n }} 小時前",
|
||||
"date-day": "昨天\n{{ n }} 天前",
|
||||
"date-week": "上週\n{{ n }} 週前",
|
||||
"date-month": "上個月\n{{ n }} 個月前",
|
||||
"date-year": "去年\n{{ n }} 年前"
|
||||
});
|
||||
|
@ -11,7 +11,8 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n",
|
||||
el = $.htmlify(jade.render("postbox", {
|
||||
"author": JSON.parse(localStorage.getItem("author")),
|
||||
"email": JSON.parse(localStorage.getItem("email")),
|
||||
"website": JSON.parse(localStorage.getItem("website"))
|
||||
"website": JSON.parse(localStorage.getItem("website")),
|
||||
"preview": ''
|
||||
}));
|
||||
|
||||
// callback on success (e.g. to toggle the reply button)
|
||||
@ -62,9 +63,27 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n",
|
||||
$("[name='author']", el).placeholder.replace(/ \(.*\)/, "");
|
||||
}
|
||||
|
||||
// preview function
|
||||
$("[name='preview']", el).on("click", function() {
|
||||
api.preview(utils.text($(".textarea", el).innerHTML)).then(
|
||||
function(html) {
|
||||
$(".preview .text", el).innerHTML = html;
|
||||
el.classList.add('preview-mode');
|
||||
});
|
||||
});
|
||||
|
||||
// edit function
|
||||
var edit = function() {
|
||||
$(".preview .text", el).innerHTML = '';
|
||||
el.classList.remove('preview-mode');
|
||||
};
|
||||
$("[name='edit']", el).on("click", edit);
|
||||
$(".preview", el).on("click", edit);
|
||||
|
||||
// submit form, initialize optional fields with `null` and reset form.
|
||||
// If replied to a comment, remove form completely.
|
||||
$("[type=submit]", el).on("click", function() {
|
||||
edit();
|
||||
if (! el.validate()) {
|
||||
return;
|
||||
}
|
||||
@ -240,7 +259,7 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n",
|
||||
$("a.edit", footer).toggle("click",
|
||||
function(toggler) {
|
||||
var edit = $("a.edit", footer);
|
||||
var avatar = config["avatar"] ? $(".avatar", el, false)[0] : null;
|
||||
var avatar = config["avatar"] || config["gravatar"] ? $(".avatar", el, false)[0] : null;
|
||||
|
||||
edit.textContent = i18n.translate("comment-save");
|
||||
edit.insertAfter($.new("a.cancel", i18n.translate("comment-cancel"))).on("click", function() {
|
||||
@ -268,7 +287,7 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n",
|
||||
},
|
||||
function(toggler) {
|
||||
var textarea = $(".textarea", text);
|
||||
var avatar = config["avatar"] ? $(".avatar", el, false)[0] : null;
|
||||
var avatar = config["avatar"] || config["gravatar"] ? $(".avatar", el, false)[0] : null;
|
||||
|
||||
if (! toggler.canceled && textarea !== null) {
|
||||
if (utils.text(textarea.innerHTML).length < 3) {
|
||||
|
@ -7,6 +7,9 @@ define(["libjs-jade-runtime", "app/utils", "jade!app/text/postbox", "jade!app/te
|
||||
var load = function(name, js) {
|
||||
templates[name] = (function(jade) {
|
||||
var fn;
|
||||
if (js.compiled) {
|
||||
return js(jade);
|
||||
}
|
||||
eval("fn = " + js);
|
||||
return fn;
|
||||
})(runtime);
|
||||
|
@ -1,4 +1,7 @@
|
||||
div(class='isso-comment' id='isso-#{comment.id}')
|
||||
if conf.gravatar
|
||||
div(class='avatar')
|
||||
img(src='#{comment.gravatar_image}')
|
||||
if conf.avatar
|
||||
div(class='avatar')
|
||||
svg(data-hash='#{comment.hash}')
|
||||
|
@ -3,6 +3,10 @@ div(class='isso-postbox')
|
||||
div(class='textarea-wrapper')
|
||||
div(class='textarea placeholder' contenteditable='true')
|
||||
= i18n('postbox-text')
|
||||
div(class='preview')
|
||||
div(class='isso-comment')
|
||||
div(class='text-wrapper')
|
||||
div(class='text')
|
||||
section(class='auth-section')
|
||||
p(class='input-wrapper')
|
||||
input(type='text' name='author' placeholder=i18n('postbox-author')
|
||||
@ -15,6 +19,12 @@ div(class='isso-postbox')
|
||||
value=website != null ? '#{website}' : '')
|
||||
p(class='post-action')
|
||||
input(type='submit' value=i18n('postbox-submit'))
|
||||
p(class='post-action')
|
||||
input(type='button' name='preview'
|
||||
value=i18n('postbox-preview'))
|
||||
p(class='post-action')
|
||||
input(type='button' name='edit'
|
||||
value=i18n('postbox-edit'))
|
||||
section(class='notification-section')
|
||||
label
|
||||
input(type='checkbox' name='notification')
|
||||
|
@ -27,6 +27,13 @@ require(["app/lib/ready", "app/config", "app/i18n", "app/api", "app/isso", "app/
|
||||
return console.log("abort, #isso-thread is missing");
|
||||
}
|
||||
|
||||
if (config["feed"]) {
|
||||
var feedLink = $.new('a', i18n.translate('atom-feed'));
|
||||
var feedLinkWrapper = $.new('span.isso-feedlink');
|
||||
feedLink.href = api.feed($("#isso-thread").getAttribute("data-isso-id"));
|
||||
feedLinkWrapper.append(feedLink);
|
||||
$("#isso-thread").append(feedLinkWrapper);
|
||||
}
|
||||
$("#isso-thread").append($.new('h4'));
|
||||
$("#isso-thread").append(new isso.Postbox(null));
|
||||
$("#isso-thread").append('<div id="isso-root"></div>');
|
||||
|
@ -49,8 +49,12 @@ define(function() {
|
||||
write: function(plugin, name, write) {
|
||||
if (builds.hasOwnProperty(name)) {
|
||||
write("define('" + plugin + "!" + name +"', function () {" +
|
||||
" var fn = " + builds[name] + ";" +
|
||||
" return fn;" +
|
||||
" var wfn = function (jade) {" +
|
||||
" var fn = " + builds[name] + ";" +
|
||||
" return fn;" +
|
||||
" };" +
|
||||
"wfn.compiled = true;" +
|
||||
"return wfn;" +
|
||||
"});\n");
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ class Progress(object):
|
||||
|
||||
if time() - self.last > 0.2:
|
||||
sys.stdout.write("\r{0}".format(" " * cols))
|
||||
sys.stdout.write("\r[{0:.0%}] {1}".format(i/self.end, message))
|
||||
sys.stdout.write("\r[{0:.0%}] {1}".format(i / self.end, message))
|
||||
sys.stdout.flush()
|
||||
self.last = time()
|
||||
|
||||
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
@ -32,6 +33,7 @@ class TestComments(unittest.TestCase):
|
||||
conf.set("general", "dbpath", self.path)
|
||||
conf.set("guard", "enabled", "off")
|
||||
conf.set("hash", "algorithm", "none")
|
||||
self.conf = conf
|
||||
|
||||
class App(Isso, core.Mixin):
|
||||
pass
|
||||
@ -132,13 +134,13 @@ class TestComments(unittest.TestCase):
|
||||
self.assertFalse(verify({"text": text}))
|
||||
|
||||
# email/website length
|
||||
self.assertTrue(verify({"text": "...", "email": "*"*254}))
|
||||
self.assertTrue(verify({"text": "...", "email": "*" * 254}))
|
||||
self.assertTrue(
|
||||
verify({"text": "...", "website": "google.de/" + "a"*128}))
|
||||
verify({"text": "...", "website": "google.de/" + "a" * 128}))
|
||||
|
||||
self.assertFalse(verify({"text": "...", "email": "*"*1024}))
|
||||
self.assertFalse(verify({"text": "...", "email": "*" * 1024}))
|
||||
self.assertFalse(
|
||||
verify({"text": "...", "website": "google.de/" + "*"*1024}))
|
||||
verify({"text": "...", "website": "google.de/" + "*" * 1024}))
|
||||
|
||||
# valid website url
|
||||
self.assertTrue(comments.isurl("example.tld"))
|
||||
@ -320,10 +322,41 @@ class TestComments(unittest.TestCase):
|
||||
rv = loads(rv.data)
|
||||
|
||||
for key in comments.API.FIELDS:
|
||||
rv.pop(key)
|
||||
if key in rv:
|
||||
rv.pop(key)
|
||||
|
||||
self.assertListEqual(list(rv.keys()), [])
|
||||
|
||||
def testNoFeed(self):
|
||||
rv = self.get('/feed?uri=%2Fpath%2Fnothing')
|
||||
self.assertEqual(rv.status_code, 404)
|
||||
|
||||
def testFeedEmpty(self):
|
||||
self.conf.set("rss", "base", "https://example.org")
|
||||
|
||||
rv = self.get('/feed?uri=%2Fpath%2Fnothing')
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
self.assertEqual(rv.headers['ETag'], '"empty"')
|
||||
data = rv.data.decode('utf-8')
|
||||
self.assertEqual(data, """<?xml version=\'1.0\' encoding=\'utf-8\'?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0"><updated>1970-01-01T01:00:00Z</updated><id>tag:example.org,2018:/isso/thread/path/nothing</id><title>Comments for example.org/path/nothing</title></feed>""")
|
||||
|
||||
def testFeed(self):
|
||||
self.conf.set("rss", "base", "https://example.org")
|
||||
|
||||
self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'First'}))
|
||||
self.post('/new?uri=%2Fpath%2F',
|
||||
data=json.dumps({'text': '*Second*', 'parent': 1}))
|
||||
|
||||
rv = self.get('/feed?uri=%2Fpath%2F')
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
self.assertEqual(rv.headers['ETag'], '"1-2"')
|
||||
data = rv.data.decode('utf-8')
|
||||
data = re.sub('[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z',
|
||||
'2018-04-01T10:00:00Z', data)
|
||||
self.assertEqual(data, """<?xml version=\'1.0\' encoding=\'utf-8\'?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0"><updated>2018-04-01T10:00:00Z</updated><id>tag:example.org,2018:/isso/thread/path/</id><title>Comments for example.org/path/</title><entry><id>tag:example.org,2018:/isso/1/2</id><title>Comment #2</title><updated>2018-04-01T10:00:00Z</updated><author><name /></author><link href="https://example.org/path/#isso-2" /><content type="html"><p><em>Second</em></p></content><thr:in-reply-to href="https://example.org/path/#isso-1" ref="tag:example.org,2018:/isso/1/1" /></entry><entry><id>tag:example.org,2018:/isso/1/1</id><title>Comment #1</title><updated>2018-04-01T10:00:00Z</updated><author><name /></author><link href="https://example.org/path/#isso-1" /><content type="html"><p>First</p></content></entry></feed>""")
|
||||
|
||||
def testCounts(self):
|
||||
|
||||
self.assertEqual(self.get('/count?uri=%2Fpath%2F').status_code, 404)
|
||||
|
@ -29,7 +29,7 @@ class TestHTML(unittest.TestCase):
|
||||
self.assertEqual(convert(input), expected)
|
||||
|
||||
def test_github_flavoured_markdown(self):
|
||||
convert = html.Markdown(extensions=("fenced_code", ))
|
||||
convert = html.Markdown(extensions=("fenced-code", ))
|
||||
|
||||
# without lang
|
||||
_in = textwrap.dedent("""\
|
||||
@ -65,13 +65,17 @@ class TestHTML(unittest.TestCase):
|
||||
examples = [
|
||||
('Look: <img src="..." />', 'Look: '),
|
||||
('<a href="http://example.org/">Ha</a>',
|
||||
'<a href="http://example.org/">Ha</a>'),
|
||||
['<a href="http://example.org/" rel="nofollow noopener">Ha</a>',
|
||||
'<a rel="nofollow noopener" href="http://example.org/">Ha</a>']),
|
||||
('<a href="sms:+1234567890">Ha</a>', '<a>Ha</a>'),
|
||||
('<p style="visibility: hidden;">Test</p>', '<p>Test</p>'),
|
||||
('<script>alert("Onoe")</script>', 'alert("Onoe")')]
|
||||
|
||||
for (input, expected) in examples:
|
||||
self.assertEqual(html.sanitize(sanitizer, input), expected)
|
||||
if isinstance(expected, list):
|
||||
self.assertIn(html.sanitize(sanitizer, input), expected)
|
||||
else:
|
||||
self.assertEqual(html.sanitize(sanitizer, input), expected)
|
||||
|
||||
@unittest.skipIf(html.HTML5LIB_VERSION <= html.HTML5LIB_SIMPLETREE, "backport")
|
||||
def test_sanitizer_extensions(self):
|
||||
@ -92,5 +96,6 @@ class TestHTML(unittest.TestCase):
|
||||
}
|
||||
})
|
||||
renderer = html.Markup(conf.section("markup")).render
|
||||
self.assertEqual(renderer("http://example.org/ and sms:+1234567890"),
|
||||
'<p><a href="http://example.org/">http://example.org/</a> and sms:+1234567890</p>')
|
||||
self.assertIn(renderer("http://example.org/ and sms:+1234567890"),
|
||||
['<p><a href="http://example.org/" rel="nofollow noopener">http://example.org/</a> and sms:+1234567890</p>',
|
||||
'<p><a rel="nofollow noopener" href="http://example.org/">http://example.org/</a> and sms:+1234567890</p>'])
|
||||
|
@ -36,7 +36,7 @@ def anonymize(remote_addr):
|
||||
ipv6 = ipaddress.IPv6Address(remote_addr)
|
||||
if ipv6.ipv4_mapped is not None:
|
||||
return anonymize(ipv6.ipv4_mapped)
|
||||
return u'' + ipv6.exploded.rsplit(':', 5)[0] + ':' + ':'.join(['0000']*5)
|
||||
return u'' + ipv6.exploded.rsplit(':', 5)[0] + ':' + ':'.join(['0000'] * 5)
|
||||
except ipaddress.AddressValueError:
|
||||
return u'0.0.0.0'
|
||||
|
||||
@ -89,11 +89,11 @@ class Bloomfilter:
|
||||
|
||||
def add(self, key):
|
||||
for i in self.get_probes(key):
|
||||
self.array[i//8] |= 2 ** (i % 8)
|
||||
self.array[i // 8] |= 2 ** (i % 8)
|
||||
self.elements += 1
|
||||
|
||||
def __contains__(self, key):
|
||||
return all(self.array[i//8] & (2 ** (i % 8)) for i in self.get_probes(key))
|
||||
return all(self.array[i // 8] & (2 ** (i % 8)) for i in self.get_probes(key))
|
||||
|
||||
def __len__(self):
|
||||
return self.elements
|
||||
@ -132,3 +132,10 @@ class JSONResponse(Response):
|
||||
kwargs["content_type"] = "application/json"
|
||||
super(JSONResponse, self).__init__(
|
||||
json.dumps(obj).encode("utf-8"), *args, **kwargs)
|
||||
|
||||
|
||||
class XMLResponse(Response):
|
||||
def __init__(self, obj, *args, **kwargs):
|
||||
kwargs["content_type"] = "text/xml"
|
||||
super(XMLResponse, self).__init__(
|
||||
obj, *args, **kwargs)
|
||||
|
@ -110,3 +110,4 @@ def new(conf):
|
||||
|
||||
|
||||
sha1 = Hash(func="sha1").uhash
|
||||
md5 = Hash(func="md5").uhash
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import operator
|
||||
import pkg_resources
|
||||
|
||||
from distutils.version import LooseVersion as Version
|
||||
@ -10,8 +9,6 @@ from distutils.version import LooseVersion as Version
|
||||
HTML5LIB_VERSION = Version(pkg_resources.get_distribution("html5lib").version)
|
||||
HTML5LIB_SIMPLETREE = Version("0.95")
|
||||
|
||||
from isso.compat import reduce
|
||||
|
||||
import html5lib
|
||||
from html5lib.sanitizer import HTMLSanitizer
|
||||
from html5lib.serializer import HTMLSerializer
|
||||
@ -23,7 +20,8 @@ def Sanitizer(elements, attributes):
|
||||
|
||||
class Inner(HTMLSanitizer):
|
||||
|
||||
# attributes found in Sundown's HTML serializer [1] except for <img> tag,
|
||||
# attributes found in Sundown's HTML serializer [1]
|
||||
# except for <img> tag,
|
||||
# because images are not generated anyways.
|
||||
#
|
||||
# [1] https://github.com/vmg/sundown/blob/master/html/html.c
|
||||
@ -50,6 +48,11 @@ def sanitize(tokenizer, document):
|
||||
|
||||
if HTML5LIB_VERSION > HTML5LIB_SIMPLETREE:
|
||||
builder = "etree"
|
||||
|
||||
for link in domtree.findall(".//{http://www.w3.org/1999/xhtml}a"):
|
||||
if link.get('href', None):
|
||||
link.set("rel", "nofollow noopener")
|
||||
|
||||
else:
|
||||
builder = "simpletree"
|
||||
|
||||
@ -60,14 +63,14 @@ def sanitize(tokenizer, document):
|
||||
return serializer.render(stream)
|
||||
|
||||
|
||||
def Markdown(extensions=("strikethrough", "superscript", "autolink")):
|
||||
def Markdown(extensions=("strikethrough", "superscript", "autolink",
|
||||
"fenced-code")):
|
||||
|
||||
flags = reduce(operator.xor, map(
|
||||
lambda ext: getattr(misaka, 'EXT_' + ext.upper()), extensions), 0)
|
||||
md = misaka.Markdown(Unofficial(), extensions=flags)
|
||||
renderer = Unofficial()
|
||||
md = misaka.Markdown(renderer, extensions=extensions)
|
||||
|
||||
def inner(text):
|
||||
rv = md.render(text).rstrip("\n")
|
||||
rv = md(text).rstrip("\n")
|
||||
if rv.startswith("<p>") or rv.endswith("</p>"):
|
||||
return rv
|
||||
return "<p>" + rv + "</p>"
|
||||
@ -83,7 +86,7 @@ class Unofficial(misaka.HtmlRenderer):
|
||||
to <code class="$lang">, compatible with Highlight.js.
|
||||
"""
|
||||
|
||||
def block_code(self, text, lang):
|
||||
def blockcode(self, text, lang):
|
||||
lang = ' class="{0}"'.format(lang) if lang else ''
|
||||
return "<pre><code{1}>{0}</code></pre>\n".format(text, lang)
|
||||
|
||||
|
@ -9,6 +9,7 @@ import functools
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from itsdangerous import SignatureExpired, BadSignature
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from werkzeug.http import dump_cookie
|
||||
from werkzeug.wsgi import get_current_url
|
||||
@ -20,10 +21,21 @@ from werkzeug.exceptions import BadRequest, Forbidden, NotFound
|
||||
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, XMLResponse as XML,
|
||||
render_template)
|
||||
from isso.views import requires
|
||||
from isso.utils.hash import sha1
|
||||
from isso.utils.hash import md5
|
||||
|
||||
try:
|
||||
from urlparse import urlparse
|
||||
except ImportError:
|
||||
from urllib.parse import urlparse
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except ImportError:
|
||||
from io import BytesIO as StringIO
|
||||
|
||||
# from Django appearently, looks good to me *duck*
|
||||
__url_re = re.compile(
|
||||
@ -81,7 +93,7 @@ def xhr(func):
|
||||
class API(object):
|
||||
|
||||
FIELDS = set(['id', 'parent', 'text', 'author', 'website',
|
||||
'mode', 'created', 'modified', 'likes', 'dislikes', 'hash', 'notification'])
|
||||
'mode', 'created', 'modified', 'likes', 'dislikes', 'hash', 'gravatar_image', 'notification'])
|
||||
|
||||
# comment fields, that can be submitted
|
||||
ACCEPT = set(['text', 'author', 'website', 'email', 'parent', 'title', 'notification'])
|
||||
@ -91,6 +103,7 @@ class API(object):
|
||||
('new', ('POST', '/new')),
|
||||
('count', ('GET', '/count')),
|
||||
('counts', ('POST', '/count')),
|
||||
('feed', ('GET', '/feed')),
|
||||
('view', ('GET', '/id/<int:id>')),
|
||||
('edit', ('PUT', '/id/<int:id>')),
|
||||
('delete', ('DELETE', '/id/<int:id>')),
|
||||
@ -292,6 +305,8 @@ class API(object):
|
||||
self.cache.set(
|
||||
'hash', (rv['email'] or rv['remote_addr']).encode('utf-8'), rv['hash'])
|
||||
|
||||
rv = self._add_gravatar_image(rv)
|
||||
|
||||
for key in set(rv.keys()) - API.FIELDS:
|
||||
rv.pop(key)
|
||||
|
||||
@ -779,6 +794,18 @@ class API(object):
|
||||
|
||||
return JSON(rv, 200)
|
||||
|
||||
def _add_gravatar_image(self, item):
|
||||
if not self.conf.getboolean('gravatar'):
|
||||
return item
|
||||
|
||||
email = item['email'] or ""
|
||||
email_md5_hash = md5(email)
|
||||
|
||||
gravatar_url = self.conf.get('gravatar-url')
|
||||
item['gravatar_image'] = gravatar_url.format(email_md5_hash)
|
||||
|
||||
return item
|
||||
|
||||
def _process_fetched_list(self, fetched_list, plain=False):
|
||||
for item in fetched_list:
|
||||
|
||||
@ -791,6 +818,8 @@ class API(object):
|
||||
|
||||
item['hash'] = val
|
||||
|
||||
item = self._add_gravatar_image(item)
|
||||
|
||||
for key in set(item.keys()) - API.FIELDS:
|
||||
item.pop(key)
|
||||
|
||||
@ -885,7 +914,6 @@ class API(object):
|
||||
@apiSuccessExample Counts of 5 threads:
|
||||
[2, 18, 4, 0, 3]
|
||||
"""
|
||||
|
||||
def counts(self, environ, request):
|
||||
|
||||
data = request.get_json()
|
||||
@ -895,6 +923,125 @@ class API(object):
|
||||
|
||||
return JSON(self.comments.count(*data), 200)
|
||||
|
||||
"""
|
||||
@api {get} /feed Atom feed for comments
|
||||
@apiGroup Thread
|
||||
@apiDescription
|
||||
Provide an Atom feed for the given thread.
|
||||
"""
|
||||
@requires(str, 'uri')
|
||||
def feed(self, environ, request, uri):
|
||||
conf = self.isso.conf.section("rss")
|
||||
if not conf.get('base'):
|
||||
raise NotFound
|
||||
|
||||
args = {
|
||||
'uri': uri,
|
||||
'order_by': 'id',
|
||||
'asc': 0,
|
||||
'limit': conf.getint('limit')
|
||||
}
|
||||
try:
|
||||
args['limit'] = max(int(request.args.get('limit')), args['limit'])
|
||||
except TypeError:
|
||||
pass
|
||||
except ValueError:
|
||||
return BadRequest("limit should be integer")
|
||||
comments = self.comments.fetch(**args)
|
||||
base = conf.get('base')
|
||||
hostname = urlparse(base).netloc
|
||||
|
||||
# Let's build an Atom feed.
|
||||
# RFC 4287: https://tools.ietf.org/html/rfc4287
|
||||
# RFC 4685: https://tools.ietf.org/html/rfc4685 (threading extensions)
|
||||
# For IDs: http://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id
|
||||
feed = ET.Element('feed', {
|
||||
'xmlns': 'http://www.w3.org/2005/Atom',
|
||||
'xmlns:thr': 'http://purl.org/syndication/thread/1.0'
|
||||
})
|
||||
|
||||
# For feed ID, we would use thread ID, but we may not have
|
||||
# one. Therefore, we use the URI. We don't have a year
|
||||
# either...
|
||||
id = ET.SubElement(feed, 'id')
|
||||
id.text = 'tag:{hostname},2018:/isso/thread{uri}'.format(
|
||||
hostname=hostname, uri=uri)
|
||||
|
||||
# For title, we don't have much either. Be pretty generic.
|
||||
title = ET.SubElement(feed, 'title')
|
||||
title.text = 'Comments for {hostname}{uri}'.format(
|
||||
hostname=hostname, uri=uri)
|
||||
|
||||
comment0 = None
|
||||
|
||||
for comment in comments:
|
||||
if comment0 is None:
|
||||
comment0 = comment
|
||||
|
||||
entry = ET.SubElement(feed, 'entry')
|
||||
# We don't use a real date in ID either to help with
|
||||
# threading.
|
||||
id = ET.SubElement(entry, 'id')
|
||||
id.text = 'tag:{hostname},2018:/isso/{tid}/{id}'.format(
|
||||
hostname=hostname,
|
||||
tid=comment['tid'],
|
||||
id=comment['id'])
|
||||
title = ET.SubElement(entry, 'title')
|
||||
title.text = 'Comment #{}'.format(comment['id'])
|
||||
updated = ET.SubElement(entry, 'updated')
|
||||
updated.text = '{}Z'.format(datetime.fromtimestamp(
|
||||
comment['modified'] or comment['created']).isoformat())
|
||||
author = ET.SubElement(entry, 'author')
|
||||
name = ET.SubElement(author, 'name')
|
||||
name.text = comment['author']
|
||||
ET.SubElement(entry, 'link', {
|
||||
'href': '{base}{uri}#isso-{id}'.format(
|
||||
base=base,
|
||||
uri=uri, id=comment['id'])
|
||||
})
|
||||
content = ET.SubElement(entry, 'content', {
|
||||
'type': 'html',
|
||||
})
|
||||
content.text = self.isso.render(comment['text'])
|
||||
|
||||
if comment['parent']:
|
||||
ET.SubElement(entry, 'thr:in-reply-to', {
|
||||
'ref': 'tag:{hostname},2018:/isso/{tid}/{id}'.format(
|
||||
hostname=hostname,
|
||||
tid=comment['tid'],
|
||||
id=comment['parent']),
|
||||
'href': '{base}{uri}#isso-{id}'.format(
|
||||
base=base,
|
||||
uri=uri, id=comment['parent'])
|
||||
})
|
||||
|
||||
# Updated is mandatory. If we have comments, we use the date
|
||||
# of last modification of the first one (which is the last
|
||||
# one). Otherwise, we use a fixed date.
|
||||
updated = ET.Element('updated')
|
||||
if comment0 is None:
|
||||
updated.text = '1970-01-01T01:00:00Z'
|
||||
else:
|
||||
updated.text = datetime.fromtimestamp(
|
||||
comment0['modified'] or comment0['created']).isoformat()
|
||||
updated.text += 'Z'
|
||||
feed.insert(0, updated)
|
||||
|
||||
output = StringIO()
|
||||
ET.ElementTree(feed).write(output,
|
||||
encoding='utf-8',
|
||||
xml_declaration=True)
|
||||
response = XML(output.getvalue(), 200)
|
||||
|
||||
# Add an etag/last-modified value for caching purpose
|
||||
if comment0 is None:
|
||||
response.set_etag('empty')
|
||||
response.last_modified = 0
|
||||
else:
|
||||
response.set_etag('{tid}-{id}'.format(**comment0))
|
||||
response.last_modified = comment0['modified'] or comment0['created']
|
||||
return response.make_conditional(request)
|
||||
|
||||
def preview(self, environment, request):
|
||||
data = request.get_json()
|
||||
|
||||
@ -904,14 +1051,19 @@ class API(object):
|
||||
return JSON({'text': self.isso.render(data["text"])}, 200)
|
||||
|
||||
def demo(self, env, req):
|
||||
return redirect(get_current_url(env) + '/index.html')
|
||||
return redirect(
|
||||
get_current_url(env, strip_querystring=True) + '/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')
|
||||
response = redirect(re.sub(
|
||||
r'/login$',
|
||||
'/admin',
|
||||
get_current_url(env, strip_querystring=True)
|
||||
))
|
||||
cookie = functools.partial(dump_cookie,
|
||||
value=self.isso.sign({"logged": True}),
|
||||
expires=datetime.now() + timedelta(1))
|
||||
|
@ -30,7 +30,7 @@ def host(environ): # pragma: no cover
|
||||
of http://www.python.org/dev/peps/pep-0333/#url-reconstruction
|
||||
"""
|
||||
|
||||
url = environ['wsgi.url_scheme']+'://'
|
||||
url = environ['wsgi.url_scheme'] + '://'
|
||||
|
||||
if environ.get('HTTP_HOST'):
|
||||
url += environ['HTTP_HOST']
|
||||
@ -84,6 +84,8 @@ def origin(hosts):
|
||||
hosts = [urlsplit(h) for h in hosts]
|
||||
|
||||
def func(environ):
|
||||
if 'ISSO_CORS_ORIGIN' in environ:
|
||||
return environ['ISSO_CORS_ORIGIN']
|
||||
|
||||
if not hosts:
|
||||
return "http://invalid.local"
|
||||
|
5
setup.py
5
setup.py
@ -5,8 +5,8 @@ import sys
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
requires = ['html5lib==0.9999999', 'itsdangerous', 'Jinja2',
|
||||
'misaka>=1.0,<2.0', 'werkzeug>=0.9']
|
||||
requires = ['itsdangerous', 'Jinja2', 'misaka>=2.0,<3.0', 'html5lib<0.9999999',
|
||||
'werkzeug>=0.9']
|
||||
|
||||
if sys.version_info < (2, 7):
|
||||
raise SystemExit("Python 2 versions < 2.7 are not supported.")
|
||||
@ -39,6 +39,7 @@ setup(
|
||||
extras_require={
|
||||
':python_version=="2.7"': ['ipaddr>=2.1', 'configparser']
|
||||
},
|
||||
setup_requires=["cffi>=1.3.0"],
|
||||
entry_points={
|
||||
'console_scripts':
|
||||
['isso = isso:main'],
|
||||
|
@ -46,6 +46,14 @@ notify = stdout
|
||||
# Log console messages to file instead of standard output.
|
||||
log-file =
|
||||
|
||||
# adds property "gravatar_image" to json response when true
|
||||
# will automatically build md5 hash by email and use "gravatar_url" to build
|
||||
# the url to the gravatar image
|
||||
gravatar = false
|
||||
|
||||
# default url for gravatar. {} is where the hash will be placed
|
||||
gravatar-url = https://www.gravatar.com/avatar/{}?d=identicon
|
||||
|
||||
# Admin access password
|
||||
admin_password = please_choose_a_strong_password
|
||||
|
||||
@ -180,3 +188,15 @@ salt = Eech7co8Ohloopo9Ol6baimi
|
||||
# strengthening. Arguments have to be in that order, but can be reduced to
|
||||
# pbkdf2:4096 for example to override the iterations only.
|
||||
algorithm = pbkdf2
|
||||
|
||||
|
||||
[rss]
|
||||
# Provide an Atom feed for each comment thread for users to subscribe to.
|
||||
|
||||
# The base URL of pages is needed to build the Atom feed. By appending
|
||||
# the URI, we should get the complete URL to use to access the page
|
||||
# with the comments. When empty, Atom feeds are disabled.
|
||||
base =
|
||||
|
||||
# Limit the number of elements to return for each thread.
|
||||
limit = 100
|
||||
|
Loading…
Reference in New Issue
Block a user