isso import FILE can import Disqus export

This commit is contained in:
posativ 2012-10-23 20:36:43 +02:00
parent ecd4c6b120
commit ac6d88f61e
5 changed files with 124 additions and 10 deletions

View File

@ -17,14 +17,25 @@ Current status: `nosetests specs/`. Ran 11 tests in 0.570s.
- simple JSON API (hence comments are JavaScript-only) - simple JSON API (hence comments are JavaScript-only)
- create comments and modify/delete within a time range - create comments and modify/delete within a time range
- Ping/Trackback support (not implemented yet) - Ping/Trackback support (not implemented yet)
- simple admin interface (not implemented yet) - simple admin interface (work in progress)
- easy integration, similar to Disqus (not implemented yet) - easy integration, similar to Disqus (work in progress)
- spam filtering using [http:bl](https://www.projecthoneypot.org/) (not implemented yet) - spam filtering using [http:bl](https://www.projecthoneypot.org/) (not implemented yet)
## Installation ## Installation
TODO TODO
## Migrating from Disqus
Go to [disqus.com](https://disqus.com/) and export your "forum" as XML. If you
use Firefox and you get a 403, try a webkit browser, Disqus did something very
weird with that download link. Next:
$ isso import /path/to/ur/dump.xml
That's it. Visit your admin page to see all threads. If it doesn't work for you,
please file in a bug report \*including\* your dump.
## API ## API
### fetch comments for /foo-bar/ ### fetch comments for /foo-bar/

View File

@ -22,16 +22,24 @@
__version__ = '0.2' __version__ = '0.2'
import sys; reload(sys)
sys.setdefaultencoding('utf-8') # we only support UTF-8 and python 2.X :-)
import io
import json import json
from os.path import join, dirname
from optparse import OptionParser, make_option, SUPPRESS_HELP
from itsdangerous import URLSafeTimedSerializer from itsdangerous import URLSafeTimedSerializer
from werkzeug.wsgi import SharedDataMiddleware
from werkzeug.routing import Map, Rule from werkzeug.routing import Map, Rule
from werkzeug.serving import run_simple from werkzeug.serving import run_simple
from werkzeug.wrappers import Request, Response from werkzeug.wrappers import Request, Response
from werkzeug.exceptions import HTTPException, NotFound, InternalServerError from werkzeug.exceptions import HTTPException, NotFound, InternalServerError
from isso import admin, comment, db from isso import admin, comment, db, migrate
from isso.utils import determine, import_object, RegexConverter, IssoEncoder from isso.utils import determine, import_object, RegexConverter, IssoEncoder
# override default json :func:`dumps`. # override default json :func:`dumps`.
@ -106,10 +114,32 @@ class Isso:
def main(): def main():
from os.path import join, dirname options = [
from werkzeug.wsgi import SharedDataMiddleware make_option("--version", action="store_true", help="print version info and exit"),
make_option("--sqlite", dest="sqlite", metavar='FILE', default="/tmp/sqlite.db",
help="use SQLite3 database"),
make_option("--port", dest="port", default=8000, help="webserver port"),
make_option("--test", dest="production", action="store_false", default=True,
help=SUPPRESS_HELP),
]
app = Isso({'SQLITE': '/tmp/sqlite.db', 'PRODUCTION': False}) parser = OptionParser(option_list=options)
app = SharedDataMiddleware(app,{ options, args = parser.parse_args()
'/static': join(dirname(__file__), 'static')})
run_simple('127.0.0.1', 8000, app, use_reloader=True) if options.version:
print 'isso', __version__
sys.exit(0)
app = Isso({'SQLITE': options.sqlite, 'PRODUCTION': options.production})
if len(args) > 0 and args[0] == 'import':
if len(args) < 2:
print 'usage: isso import FILE'
sys.exit(2)
with io.open(args[1], encoding='utf-8') as fp:
migrate.disqus(app.db, fp.read())
else:
app = SharedDataMiddleware(app, {
'/static': join(dirname(__file__), 'static')})
run_simple('127.0.0.1', 8000, app, use_reloader=True)

View File

@ -107,7 +107,7 @@ class SQLite(Abstract):
with sqlite3.connect(self.dbpath) as con: with sqlite3.connect(self.dbpath) as con:
return self.query2comment( return self.query2comment(
con.execute('SELECT *, MAX(id) FROM comments;').fetchone()) con.execute('SELECT *, MAX(id) FROM comments WHERE path=?;', (path, )).fetchone())
def activate(self, path, id): def activate(self, path, id):
with sqlite3.connect(self.dbpath) as con: with sqlite3.connect(self.dbpath) as con:

66
isso/migrate.py Normal file
View File

@ -0,0 +1,66 @@
# -*- encoding: utf-8 -*-
#
# Copyright 2012, Martin Zimmermann <info@posativ.org>. All rights reserved.
# License: BSD Style, 2 clauses. see isso/__init__.py
#
# TODO
#
# - export does not include website from commenters
# - Disqus includes already deleted comments
from time import mktime, strptime
from urlparse import urlparse
from collections import defaultdict
from xml.etree import ElementTree
from isso.models import Comment
ns = '{http://disqus.com}'
dsq = '{http://disqus.com/disqus-internals}'
def insert(db, thread, comments):
path = urlparse(thread.find('%sid' % ns).text).path
remap = dict()
for item in sorted(comments, key=lambda k: k['created']):
parent = remap.get(item.get('dsq:parent'))
comment = Comment(created=item['created'], text=item['text'],
author=item['author'], email=item['email'], parent=parent)
rv = db.add(path, comment)
print rv.id, rv.text[:25], rv.author
remap[item['dsq:id']] = rv.id
def disqus(db, xml):
tree = ElementTree.fromstring(xml)
res = defaultdict(list)
for post in tree.findall('%spost' % ns):
item = {
'dsq:id': post.attrib.get(dsq + 'id'),
'text': post.find('%smessage' % ns).text,
'author': post.find('%sauthor/%sname' % (ns, ns)).text,
'email': post.find('%sauthor/%semail' % (ns, ns)).text,
'created': mktime(strptime(
post.find('%screatedAt' % ns).text, '%Y-%m-%dT%H:%M:%SZ'))
}
if post.find(ns + 'parent') is not None:
item['dsq:parent'] = post.find(ns + 'parent').attrib.get(dsq + 'id')
res[post.find('%sthread' % ns).attrib.get(dsq + 'id')].append(item)
for thread in tree.findall('%sthread' % ns):
id = thread.attrib.get(dsq + 'id')
if id in res:
insert(db, thread, res[id])
# for comment in res[_id]:
# print ' ', comment['author'], comment['text'][:25]

View File

@ -46,6 +46,13 @@ class TestSQLite(unittest.TestCase):
assert rv[0].id == 1 assert rv[0].id == 1
assert rv[0].text == 'Baz' assert rv[0].text == 'Baz'
def test_add_return(self):
self.db.add('/', comment(text='1'))
self.db.add('/', comment(text='2'))
assert self.db.add('/path/', comment(text='1')).id == 1
def test_update(self): def test_update(self):
rv = self.db.add('/', comment(text='Foo')) rv = self.db.add('/', comment(text='Foo'))