diff --git a/Makefile b/Makefile index 8510d75..22fa937 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ ISSO_JS_DST := isso/js/embed.min.js isso/js/embed.dev.js \ ISSO_CSS := isso/css/isso.css -ISSO_PY_SRC := $(shell git ls-files | grep .py) +ISSO_PY_SRC := $(shell git ls-files | grep -E "^isso/.+.py$$") DOCS_RST_SRC := $(shell find docs/ -type f -name '*.rst') \ $(wildcard docs/_isso/*) \ @@ -30,6 +30,12 @@ all: man js site init: (cd isso/js; bower install almond requirejs requirejs-text jade) +check: + @echo "Python 2.x" + -@python2 -m pyflakes $(ISSO_PY_SRC) + @echo "Python 3.x" + -@python3 -m pyflakes $(ISSO_PY_SRC) + isso/js/%.min.js: $(ISSO_JS_SRC) $(ISSO_CSS) r.js -o isso/js/build.$*.js out=$@ diff --git a/docs/docs/configuration/server.rst b/docs/docs/configuration/server.rst index db668af..68baa2f 100644 --- a/docs/docs/configuration/server.rst +++ b/docs/docs/configuration/server.rst @@ -81,7 +81,8 @@ notify Available backends: stdout - Log to standard output. Default, if none selected. + Log to standard output. Default, if none selected. Note, this + functionality is broken since a few releases. smtp Send notifications via SMTP on new comments with activation (if diff --git a/docs/docs/extras/api.rst b/docs/docs/extras/api.rst index 366eca1..4c9f1d7 100644 --- a/docs/docs/extras/api.rst +++ b/docs/docs/extras/api.rst @@ -23,7 +23,6 @@ Isso: "mode": 1, "hash": "4505c1eeda98", "author": null, - "email": null, "website": null "created": 1387321261.572392, "modified": null, diff --git a/docs/docs/install.rst b/docs/docs/install.rst index fb5415a..72d3689 100644 --- a/docs/docs/install.rst +++ b/docs/docs/install.rst @@ -49,8 +49,8 @@ but not recommended): .. code-block:: sh - ~> virtualenv /path/to/isso - ~> source /path/to/isso/bin/activate + ~> virtualenv /opt/isso + ~> source /opt/isso/bin/activate After calling `source`, you can now install packages from PyPi locally into this virtual environment. If you don't like Isso anymore, you just `rm -rf` the @@ -58,6 +58,11 @@ folder. Inside this virtual environment, you may also execute the example commands from above to upgrade your Python Package Manager (although it barely makes sense), it is completely independent from your global system. +To use Isso installed in a virtual environment outside of the virtual +environment, you just need to add */opt/isso/bin* to your :envvar:`PATH` or +execute */opt/isso/bin/isso* directly. It will launch Isso from within the +virtual environment. + With a virtualenv active, you may now continue to :ref:`install-from-pypi`! Of course you may not need a virtualenv when you are running dedicated virtual machines or a shared host (e.g. Uberspace.de). @@ -112,7 +117,7 @@ For easier execution, you can symlink the executable to a location in your .. code-block:: sh - ~> ln -s /path/to/isso-venv/bin/isso /usr/local/bin/isso + ~> ln -s /opt/isso/bin/isso /usr/local/bin/isso Upgrade ^^^^^^^ @@ -121,7 +126,7 @@ To upgrade Isso, activate your virtual environment again, and run .. code-block:: sh - ~> source /path/to/isso/bin/activate # optional + ~> source /opt/isso/bin/activate # optional ~> pip install --upgrade isso .. _prebuilt-package: @@ -205,7 +210,54 @@ Init scripts to run Isso as a service (check your distribution's documentation for your init-system; e.g. Debian uses SysVinit, Fedora uses SystemD) if you don't use FastCGi or uWSGI: -- SystemD: https://github.com/jgraichen/debian-isso/blob/master/debian/isso.service -- SysVinit: https://github.com/jgraichen/debian-isso/blob/master/debian/isso.init +- SystemD (Isso + Gunicorn): https://github.com/jgraichen/debian-isso/blob/master/debian/isso.service +- SysVinit (Isso + Gunicorn): https://github.com/jgraichen/debian-isso/blob/master/debian/isso.init - OpenBSD: https://gist.github.com/noqqe/7397719 - Supervisor: https://github.com/posativ/isso/issues/47 + +If you're writing your own init script, you can utilize ``start-stop-daemon`` +to run Isso in the background (Isso runs in the foreground usually). Below you +find a very basic SysVinit script which you can use for inspiration: + +.. code-block:: sh + + #!/bin/sh + ### BEGIN INIT INFO + # Provides: isso + # Required-Start: $local_fs $network + # Default-Start: 2 3 4 5 + # Default-Stop: 0 1 6 + # Description: lightweight Disqus alternative + ### END INIT INFO + + EXEC=/opt/isso/bin/isso + EXEC_OPTS="-c /etc/isso.cfg" + + RUNAS=isso + PIDFILE=/var/run/isso.pid + + start() { + echo 'Starting service…' >&2 + start-stop-daemon --start --user "$RUNAS" --background --make-pidfile --pidfile $PIDFILE \ + --exec $EXEC -- $EXEC_OPTS + } + + stop() { + echo 'Stopping service…' >&2 + start-stop-daemon --stop --user "$RUNAS" --pidfile $PIDFILE --exec $EXEC + } + + case "$1" in + start) + start + ;; + stop) + stop + ;; + retart) + stop + start + ;; + *) + echo "Usage: $0 {start|stop|restart}" + esac diff --git a/docs/docs/troubleshooting.rst b/docs/docs/troubleshooting.rst index 21514da..20ee8dd 100644 --- a/docs/docs/troubleshooting.rst +++ b/docs/docs/troubleshooting.rst @@ -12,6 +12,21 @@ Install Isso in a virtual environment as described in :ref:`install-interludium`. Alternatively, you can use `pip install --user` to install Isso into the user's home. +UnicodeDecodeError: 'ascii' codec can't decode byte 0xff +-------------------------------------------------------- + +Likely an issue with your environment, check you set your preferred file +encoding either in :envvar:`LANG`, :envvar:`LANGUAGE`, :envvar:`LC_ALL` or +:envvar:`LC_CTYPE`: + +.. code-block:: text + + $ env LANG=C.UTF-8 isso [-h] [--version] ... + +If none of the mentioned variables is set, the interaction with Isso will +likely fail (unable to print non-ascii characters to stdout/err, unable to +parse configuration file with non-ascii characters and to forth). + The web console shows 404 Not Found responses --------------------------------------------- diff --git a/isso/__init__.py b/isso/__init__.py index 85cbaad..bcd56ad 100644 --- a/isso/__init__.py +++ b/isso/__init__.py @@ -211,6 +211,8 @@ def main(): help="perform a trial run with no changes made") imprt.add_argument("-t", "--type", dest="type", default=None, choices=["disqus", "wordpress"], help="export type") + imprt.add_argument("--empty-id", dest="empty_id", action="store_true", + help="workaround for weird Disqus XML exports, #135") serve = subparser.add_parser("run", help="run server") @@ -227,7 +229,7 @@ def main(): dbpath = conf.get("general", "dbpath") mydb = db.SQLite3(dbpath, conf) - migrate.dispatch(args.type, mydb, args.dump) + migrate.dispatch(args.type, mydb, args.dump, args.empty_id) sys.exit(0) diff --git a/isso/db/__init__.py b/isso/db/__init__.py index e1f0485..919137c 100644 --- a/isso/db/__init__.py +++ b/isso/db/__init__.py @@ -9,6 +9,8 @@ from collections import defaultdict logger = logging.getLogger("isso") +from isso.compat import buffer + from isso.db.comments import Comments from isso.db.threads import Threads from isso.db.spam import Guard diff --git a/isso/js/app/i18n/bg.js b/isso/js/app/i18n/bg.js new file mode 100644 index 0000000..45ac24b --- /dev/null +++ b/isso/js/app/i18n/bg.js @@ -0,0 +1,27 @@ +define({ + "postbox-text": "Въведете коментара си тук (поне 3 знака)", + "postbox-author": "Име/псевдоним (незадължително)", + "postbox-email": "Ел. поща (незадължително)", + "postbox-website": "Уебсайт (незадължително)", + "postbox-submit": "Публикуване", + "num-comments": "1 коментар\n{{ n }} коментара", + "no-comments": "Все още няма коментари", + "comment-reply": "Отговор", + "comment-edit": "Редактиране", + "comment-save": "Запис", + "comment-delete": "Изтриване", + "comment-confirm": "Потвърждение", + "comment-close": "Затваряне", + "comment-cancel": "Отказ", + "comment-deleted": "Коментарът е изтрит.", + "comment-queued": "Коментарът чака на опашката за модериране.", + "comment-anonymous": "анонимен", + "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 }} години" +}); diff --git a/isso/js/app/i18n/it.js b/isso/js/app/i18n/it.js index 7a8bf51..31eeb2c 100644 --- a/isso/js/app/i18n/it.js +++ b/isso/js/app/i18n/it.js @@ -2,6 +2,7 @@ define({ "postbox-text": "Scrivi un commento qui (minimo 3 caratteri)", "postbox-author": "Nome (opzionale)", "postbox-email": "E-mail (opzionale)", + "postbox-website": "Sito web (opzionale)", "postbox-submit": "Invia", "num-comments": "Un Commento\n{{ n }} Commenti", "no-comments": "Ancora Nessun Commento", @@ -15,6 +16,7 @@ define({ "comment-deleted": "Commento eliminato.", "comment-queued": "Commento in coda per moderazione.", "comment-anonymous": "Anonimo", + "comment-hidden": "{{ n }} Nascosto", "date-now": "poco fa", "date-minute": "un minuto fa\n{{ n }} minuti fa", "date-hour": "un ora fa\n{{ n }} ore fa", diff --git a/isso/js/app/isso.js b/isso/js/app/isso.js index 8085cfd..e95f682 100644 --- a/isso/js/app/isso.js +++ b/isso/js/app/isso.js @@ -7,7 +7,8 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", var Postbox = function(parent) { - var el = $.htmlify(jade.render("postbox", { + var localStorage = utils.localStorageImpl, + el = $.htmlify(jade.render("postbox", { "author": JSON.parse(localStorage.getItem("author")), "email": JSON.parse(localStorage.getItem("email")), "website": JSON.parse(localStorage.getItem("website")) @@ -184,7 +185,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 = $(".avatar", el, false)[0]; + var avatar = config["avatar"] ? $(".avatar", el, false)[0] : null; edit.textContent = i18n.translate("comment-save"); edit.insertAfter($.new("a.cancel", i18n.translate("comment-cancel"))).on("click", function() { @@ -212,7 +213,7 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n", }, function(toggler) { var textarea = $(".textarea", text); - var avatar = $(".avatar", el, false)[0]; + var avatar = config["avatar"] ? $(".avatar", el, false)[0] : null; if (! toggler.canceled && textarea !== null) { if (utils.text(textarea.innerHTML).length < 3) { diff --git a/isso/js/app/utils.js b/isso/js/app/utils.js index d7bddf4..f971770 100644 --- a/isso/js/app/utils.js +++ b/isso/js/app/utils.js @@ -68,11 +68,34 @@ define(["app/i18n"], function(i18n) { .replace(/\n/gi, '
'); }; + // Safari private browsing mode supports localStorage, but throws QUOTA_EXCEEDED_ERR + var localStorageImpl; + try { + localStorage.setItem("x", "y"); + localStorage.removeItem("x"); + localStorageImpl = localStorage; + } catch (ex) { + localStorageImpl = (function(storage) { + return { + setItem: function(key, val) { + storage[key] = val; + }, + getItem: function(key) { + return typeof(storage[key]) !== 'undefined' ? storage[key] : null; + }, + removeItem: function(key) { + delete storage[key]; + } + }; + })({}); + } + return { cookie: cookie, pad: pad, ago: ago, text: text, - detext: detext + detext: detext, + localStorageImpl: localStorageImpl }; }); diff --git a/isso/migrate.py b/isso/migrate.py index c3f0500..f53341a 100644 --- a/isso/migrate.py +++ b/isso/migrate.py @@ -8,6 +8,7 @@ import io import re import logging import textwrap +import functools from time import mktime, strptime, time from collections import defaultdict @@ -67,12 +68,13 @@ class Disqus(object): ns = '{http://disqus.com}' internals = '{http://disqus.com/disqus-internals}' - def __init__(self, db, xmlfile): + def __init__(self, db, xmlfile, empty_id=False): self.threads = set([]) self.comments = set([]) self.db = db self.xmlfile = xmlfile + self.empty_id = empty_id def insert(self, thread, posts): @@ -119,7 +121,7 @@ class Disqus(object): progress.update(i, thread.find(Disqus.ns + 'id').text) # skip (possibly?) duplicate, but empty thread elements - if thread.find(Disqus.ns + 'id').text is None: + if thread.find(Disqus.ns + 'id').text is None and not self.empty_id: continue id = thread.attrib.get(Disqus.internals + 'id') @@ -134,7 +136,9 @@ class Disqus(object): len(self.threads), len(self.comments))) orphans = set(map(lambda e: e.attrib.get(Disqus.internals + "id"), tree.findall(Disqus.ns + "post"))) - self.comments - if orphans: + if orphans and not self.threads: + print("Isso couldn't import any thread, try again with --empty-id") + elif orphans: print("Found %i orphans:" % len(orphans)) for post in tree.findall(Disqus.ns + "post"): if post.attrib.get(Disqus.internals + "id") not in orphans: @@ -158,7 +162,7 @@ class WordPress(object): self.xmlfile = xmlfile self.count = 0 - for line in io.open(xmlfile): + for line in io.open(xmlfile, encoding="utf-8"): m = WordPress.detect(line) if m: self.ns = WordPress.ns.replace("1.0", m.group(1)) @@ -253,7 +257,7 @@ def autodetect(peek): return None -def dispatch(type, db, dump): +def dispatch(type, db, dump, empty_id=False): if db.execute("SELECT * FROM comments").fetchone(): if input("Isso DB is not empty! Continue? [y/N]: ") not in ("y", "Y"): raise SystemExit("Abort.") @@ -263,10 +267,13 @@ def dispatch(type, db, dump): elif type == "wordpress": cls = WordPress else: - with io.open(dump) as fp: + with io.open(dump, encoding="utf-8") as fp: cls = autodetect(fp.read(io.DEFAULT_BUFFER_SIZE)) if cls is None: raise SystemExit("Unknown format, abort.") + if cls is Disqus: + cls = functools.partial(cls, empty_id=empty_id) + cls(db, dump).migrate()