some documentation and move login to /admin/
This commit is contained in:
parent
588a8c306b
commit
a753045f8b
10
README
10
README
@ -3,9 +3,9 @@ Isso – Ich schrei sonst
|
|||||||
|
|
||||||
You love static blog generators (especially [Acrylamid][1] *cough*) and the
|
You love static blog generators (especially [Acrylamid][1] *cough*) and the
|
||||||
only option to interact with the community is [Disqus][2]. There's nothing
|
only option to interact with the community is [Disqus][2]. There's nothing
|
||||||
wrong with it, but if you care about the privacy of your audience you should
|
wrong with it, but if you care about the privacy of your audience you are
|
||||||
better use a comment system that is under your control. This is, were Isso
|
better off with a comment system that is under your control. This is, were
|
||||||
comes into play.
|
Isso comes into play.
|
||||||
|
|
||||||
[1]: https://github.com/posativ/acrylamid
|
[1]: https://github.com/posativ/acrylamid
|
||||||
[2]: http://disqus.com/
|
[2]: http://disqus.com/
|
||||||
@ -24,7 +24,7 @@ Features/Roadmap
|
|||||||
- [x] simple JSON API, hence comments are JavaScript-only
|
- [x] simple JSON API, hence comments are JavaScript-only
|
||||||
- [x] create comments and modify/delete within a time range as user
|
- [x] create comments and modify/delete within a time range as user
|
||||||
- [ ] Ping/Trackback support
|
- [ ] Ping/Trackback support
|
||||||
- [w] simple admin interface
|
- [x] simple admin interface
|
||||||
- [w] easy integration, similar to Disqus
|
- [w] easy integration, similar to Disqus
|
||||||
- [ ] spam filtering using [http:bl][3]
|
- [ ] spam filtering using [http:bl][3]
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ Development
|
|||||||
-----------
|
-----------
|
||||||
|
|
||||||
*Note:* This project is proudly made with the Not Invented Here syndrome,
|
*Note:* This project is proudly made with the Not Invented Here syndrome,
|
||||||
instead of `werkzeug` or `bottle` it uses about 100 lines to manage WSGI
|
instead of `werkzeug` or `bottle` it uses about 150 lines to manage WSGI
|
||||||
and instead of JQuery it uses ender.js.
|
and instead of JQuery it uses ender.js.
|
||||||
|
|
||||||
You'll need [2.6 ≤ python ≤ 2.7][4], [ender.js][5] and [YUI Compressor][6].
|
You'll need [2.6 ≤ python ≤ 2.7][4], [ender.js][5] and [YUI Compressor][6].
|
||||||
|
@ -28,9 +28,10 @@ sys.setdefaultencoding('utf-8') # we only support UTF-8 and python 2.X :-)
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import locale
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from optparse import OptionParser, make_option, SUPPRESS_HELP
|
from optparse import OptionParser, make_option
|
||||||
|
|
||||||
from itsdangerous import URLSafeTimedSerializer
|
from itsdangerous import URLSafeTimedSerializer
|
||||||
|
|
||||||
@ -41,6 +42,9 @@ from isso.utils import determine, import_object, IssoEncoder
|
|||||||
_dumps = json.dumps
|
_dumps = json.dumps
|
||||||
setattr(json, 'dumps', lambda obj, **kw: _dumps(obj, cls=IssoEncoder, **kw))
|
setattr(json, 'dumps', lambda obj, **kw: _dumps(obj, cls=IssoEncoder, **kw))
|
||||||
|
|
||||||
|
# set user's preferred locale, XXX conflicts with email.util.parse_date ... m(
|
||||||
|
# locale.setlocale(locale.LC_ALL, '')
|
||||||
|
|
||||||
|
|
||||||
class Isso(object):
|
class Isso(object):
|
||||||
|
|
||||||
@ -74,7 +78,6 @@ class Isso(object):
|
|||||||
lambda r: (wsgi.Rule(r[0]), r[1], r[2] if isinstance(r[2], list) else [r[2]]), [
|
lambda r: (wsgi.Rule(r[0]), r[1], r[2] if isinstance(r[2], list) else [r[2]]), [
|
||||||
|
|
||||||
# moderation panel
|
# moderation panel
|
||||||
('/', admin.login, ['HEAD', 'GET', 'POST']),
|
|
||||||
('/admin/', admin.index, ['HEAD', 'GET', 'POST']),
|
('/admin/', admin.index, ['HEAD', 'GET', 'POST']),
|
||||||
|
|
||||||
# assets
|
# assets
|
||||||
@ -86,7 +89,6 @@ class Isso(object):
|
|||||||
('/1.0/<(.+?):path>/<(int):id>', comment.get, ['HEAD', 'GET']),
|
('/1.0/<(.+?):path>/<(int):id>', comment.get, ['HEAD', 'GET']),
|
||||||
('/1.0/<(.+?):path>/<(int):id>', comment.modify, ['PUT', 'DELETE']),
|
('/1.0/<(.+?):path>/<(int):id>', comment.modify, ['PUT', 'DELETE']),
|
||||||
('/1.0/<(.+?):path>/<(int):id>/approve', comment.approve, 'PUT'),
|
('/1.0/<(.+?):path>/<(int):id>/approve', comment.approve, 'PUT'),
|
||||||
|
|
||||||
('/1.0/<(.+?):path>', comment.get, 'GET'),
|
('/1.0/<(.+?):path>', comment.get, 'GET'),
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -174,8 +176,6 @@ def main():
|
|||||||
make_option("--sqlite", dest="sqlite", metavar='FILE', default="/tmp/sqlite.db",
|
make_option("--sqlite", dest="sqlite", metavar='FILE', default="/tmp/sqlite.db",
|
||||||
help="use SQLite3 database"),
|
help="use SQLite3 database"),
|
||||||
make_option("--port", dest="port", default=8000, help="webserver port"),
|
make_option("--port", dest="port", default=8000, help="webserver port"),
|
||||||
make_option("--debug", dest="production", action="store_false", default=True,
|
|
||||||
help=SUPPRESS_HELP),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
parser = OptionParser(option_list=options)
|
parser = OptionParser(option_list=options)
|
||||||
@ -185,7 +185,7 @@ def main():
|
|||||||
print 'isso', __version__
|
print 'isso', __version__
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
app = Isso({'SQLITE': options.sqlite, 'PRODUCTION': options.production, 'MODERATION': True})
|
app = Isso({'SQLITE': options.sqlite, 'MODERATION': True})
|
||||||
|
|
||||||
if len(args) > 0 and args[0] == 'import':
|
if len(args) > 0 and args[0] == 'import':
|
||||||
if len(args) < 2:
|
if len(args) < 2:
|
||||||
@ -195,7 +195,8 @@ def main():
|
|||||||
with io.open(args[1], encoding='utf-8') as fp:
|
with io.open(args[1], encoding='utf-8') as fp:
|
||||||
migrate.disqus(app.db, fp.read())
|
migrate.disqus(app.db, fp.read())
|
||||||
|
|
||||||
else:
|
sys.exit(0)
|
||||||
from wsgiref.simple_server import make_server
|
|
||||||
httpd = make_server('127.0.0.1', 8080, app, server_class=wsgi.ThreadedWSGIServer)
|
from wsgiref.simple_server import make_server
|
||||||
httpd.serve_forever()
|
httpd = make_server('127.0.0.1', 8080, app, server_class=wsgi.ThreadedWSGIServer)
|
||||||
|
httpd.serve_forever()
|
||||||
|
@ -15,25 +15,20 @@ mako = TemplateLookup(directories=[join(dirname(__file__), 'templates')], input_
|
|||||||
render = lambda template, **context: mako.get_template(template).render_unicode(**context)
|
render = lambda template, **context: mako.get_template(template).render_unicode(**context)
|
||||||
|
|
||||||
|
|
||||||
def login(app, environ, request):
|
def index(app, environ, request):
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
if request.form.getfirst('secret') == app.SECRET:
|
if request.form.getfirst('secret') == app.SECRET:
|
||||||
return 301, '', {
|
return 301, '', {
|
||||||
'Location': '/admin/',
|
'Location': '/admin/',
|
||||||
'Set-Cookie': setcookie('admin', app.signer.dumps('*'),
|
'Set-Cookie': setcookie('admin', app.signer.dumps('*'),
|
||||||
max_age=app.MAX_AGE, path='/')
|
max_age=app.MAX_AGE, path='/')}
|
||||||
}
|
return 403, '', {}
|
||||||
|
else:
|
||||||
return 200, render('login.mako').encode('utf-8'), {'Content-Type': 'text/html'}
|
try:
|
||||||
|
app.unsign(request.cookies.get('admin', ''))
|
||||||
|
except (SignatureExpired, BadSignature):
|
||||||
def index(app, environ, request):
|
return 200, render('login.mako').encode('utf-8'), {'Content-Type': 'text/html'}
|
||||||
|
|
||||||
try:
|
|
||||||
app.unsign(request.cookies.get('admin', ''))
|
|
||||||
except (SignatureExpired, BadSignature):
|
|
||||||
return 301, '', {'Location': '/'}
|
|
||||||
|
|
||||||
ctx = {'app': app, 'request': request}
|
ctx = {'app': app, 'request': request}
|
||||||
return 200, render('admin.mako', **ctx).encode('utf-8'), {'Content-Type': 'text/html'}
|
return 200, render('admin.mako', **ctx).encode('utf-8'), {'Content-Type': 'text/html'}
|
||||||
|
@ -14,7 +14,6 @@ from isso import json, models, utils, wsgi
|
|||||||
def create(app, environ, request, path):
|
def create(app, environ, request, path):
|
||||||
|
|
||||||
if app.PRODUCTION and not utils.urlexists(app.HOST, path):
|
if app.PRODUCTION and not utils.urlexists(app.HOST, path):
|
||||||
print app.HOST, path
|
|
||||||
return 400, 'URL does not exist', {}
|
return 400, 'URL does not exist', {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
|
|
||||||
<!-- login form -->
|
<!-- login form -->
|
||||||
<div id="login">
|
<div id="login">
|
||||||
<form action="/" method="post">
|
<form action="/admin/" method="post">
|
||||||
<input name="secret" placeholder="secret" type="password" />
|
<input name="secret" placeholder="secret" type="password" />
|
||||||
<input type="submit" name="submit" value="Login" class="button" />
|
<input type="submit" name="submit" value="Login" class="button" />
|
||||||
</form>
|
</form>
|
||||||
|
16
isso/wsgi.py
16
isso/wsgi.py
@ -21,6 +21,8 @@ from wsgiref.simple_server import WSGIServer
|
|||||||
|
|
||||||
|
|
||||||
class Request(object):
|
class Request(object):
|
||||||
|
"""A ``werkzeug.wrappers.Request``-like object but much less powerful.
|
||||||
|
Fits exactly the needs of Isso."""
|
||||||
|
|
||||||
def __init__(self, environ):
|
def __init__(self, environ):
|
||||||
|
|
||||||
@ -66,6 +68,14 @@ class Request(object):
|
|||||||
|
|
||||||
|
|
||||||
class Rule(str):
|
class Rule(str):
|
||||||
|
"""A quick and dirty approach to URL route creation. It uses the
|
||||||
|
following format:
|
||||||
|
|
||||||
|
- ``<(int):name>`` matches any integer, same for float
|
||||||
|
- ``<(.+?):path>`` matches any given regular expression
|
||||||
|
|
||||||
|
With ``Rule.match(url)`` you can test whether a route a) matches
|
||||||
|
and if so b) retrieve saved variables."""
|
||||||
|
|
||||||
repl = {'int': r'[0-9]+', 'float': r'\-?[0-9]+\.[0-9]+'}
|
repl = {'int': r'[0-9]+', 'float': r'\-?[0-9]+\.[0-9]+'}
|
||||||
|
|
||||||
@ -94,7 +104,7 @@ class Rule(str):
|
|||||||
|
|
||||||
kwargs = match.groupdict()
|
kwargs = match.groupdict()
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
for type in int, float:
|
for type in int, float: # may convert false-positives
|
||||||
try:
|
try:
|
||||||
kwargs[key] = type(value)
|
kwargs[key] = type(value)
|
||||||
break
|
break
|
||||||
@ -105,6 +115,9 @@ class Rule(str):
|
|||||||
|
|
||||||
|
|
||||||
def sendfile(filename, root, environ):
|
def sendfile(filename, root, environ):
|
||||||
|
"""Return file object if found. This function is heavily inspired by
|
||||||
|
bottles's `static_file` function and uses the same mechanism to avoid
|
||||||
|
access to e.g. `/etc/shadow`."""
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
root = abspath(root) + os.sep
|
root = abspath(root) + os.sep
|
||||||
@ -133,6 +146,7 @@ def sendfile(filename, root, environ):
|
|||||||
|
|
||||||
|
|
||||||
def static(app, environ, request, directory, path):
|
def static(app, environ, request, directory, path):
|
||||||
|
"""A view that returns the requested path from directory."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return sendfile(path, join(dirname(__file__), directory), environ)
|
return sendfile(path, join(dirname(__file__), directory), environ)
|
||||||
|
Loading…
Reference in New Issue
Block a user