Merge branch 'legacy/0.9'

Conflicts:
	isso/migrate.py
This commit is contained in:
Martin Zimmermann 2014-12-25 21:53:31 +01:00
commit d70eb160b9
12 changed files with 157 additions and 20 deletions

View File

@ -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=$@

View File

@ -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

View File

@ -23,7 +23,6 @@ Isso:
"mode": 1,
"hash": "4505c1eeda98",
"author": null,
"email": null,
"website": null
"created": 1387321261.572392,
"modified": null,

View File

@ -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

View File

@ -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
---------------------------------------------

View File

@ -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)

View File

@ -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

27
isso/js/app/i18n/bg.js Normal file
View File

@ -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 }} години"
});

View File

@ -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",

View File

@ -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) {

View File

@ -68,11 +68,34 @@ define(["app/i18n"], function(i18n) {
.replace(/\n/gi, '<br>');
};
// 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
};
});

View File

@ -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()