better admin 'theme', 304 Not Modified support and minor improvements

This commit is contained in:
posativ 2012-12-16 17:59:17 +01:00
parent 5968a977ab
commit 65c2fce636
9 changed files with 214 additions and 60 deletions

2
README
View File

@ -1,7 +1,7 @@
Isso Ich schrei sonst 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 should
better use a comment system that is under your control. This is, were Isso better use a comment system that is under your control. This is, were Isso

View File

@ -54,7 +54,8 @@ class Isso(object):
MAX_AGE = 15 * 60 MAX_AGE = 15 * 60
HTTP_STATUS_CODES = { HTTP_STATUS_CODES = {
200: 'Ok', 201: 'Created', 202: 'Accepted', 301: 'Moved Permanently', 200: 'Ok', 201: 'Created', 202: 'Accepted',
301: 'Moved Permanently', 304: 'Not Modified',
400: 'Bad Request', 404: 'Not Found', 403: 'Forbidden', 400: 'Bad Request', 404: 'Not Found', 403: 'Forbidden',
500: 'Internal Server Error', 500: 'Internal Server Error',
} }
@ -73,13 +74,16 @@ 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, ['GET', 'POST']), ('/', admin.login, ['HEAD', 'GET', 'POST']),
('/admin/', admin.index, ['GET', 'POST']), ('/admin/', admin.index, ['HEAD', 'GET', 'POST']),
# assets
('/<(static|js):directory>/<(.+?):path>', wsgi.static, ['HEAD', 'GET']),
# comment API, note that the client side quotes the URL, but this is # comment API, note that the client side quotes the URL, but this is
# actually unnecessary. PEP 333 aka WSGI always unquotes PATH_INFO. # actually unnecessary. PEP 333 aka WSGI always unquotes PATH_INFO.
('/1.0/<(.+?):path>/new', comment.create, 'POST'), ('/1.0/<(.+?):path>/new', comment.create,'POST'),
('/1.0/<(.+?):path>/<(int):id>', comment.get, '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'),
@ -117,11 +121,11 @@ class Isso(object):
if code == 404: if code == 404:
try: try:
code, body, headers = wsgi.sendfile(environ['PATH_INFO'], os.getcwd()) code, body, headers = wsgi.sendfile(environ['PATH_INFO'], os.getcwd(), environ)
except (IOError, OSError): except (IOError, OSError):
try: try:
path = environ['PATH_INFO'].rstrip('/') + '/index.html' path = environ['PATH_INFO'].rstrip('/') + '/index.html'
code, body, headers = wsgi.sendfile(path, os.getcwd()) code, body, headers = wsgi.sendfile(path, os.getcwd(), environ)
except (IOError, OSError): except (IOError, OSError):
pass pass
@ -181,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': False}) app = Isso({'SQLITE': options.sqlite, 'PRODUCTION': options.production, '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:

View File

@ -3,13 +3,15 @@
# Copyright 2012, Martin Zimmermann <info@posativ.org>. All rights reserved. # Copyright 2012, Martin Zimmermann <info@posativ.org>. All rights reserved.
# License: BSD Style, 2 clauses. see isso/__init__.py # License: BSD Style, 2 clauses. see isso/__init__.py
from os.path import join, dirname
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
from itsdangerous import SignatureExpired, BadSignature from itsdangerous import SignatureExpired, BadSignature
from isso.wsgi import setcookie from isso.wsgi import setcookie
mako = TemplateLookup(directories=['isso/templates'], input_encoding='utf-8') mako = TemplateLookup(directories=[join(dirname(__file__), 'templates')], input_encoding='utf-8')
render = lambda template, **context: mako.get_template(template).render_unicode(**context) render = lambda template, **context: mako.get_template(template).render_unicode(**context)
@ -19,7 +21,7 @@ def login(app, environ, request):
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('session-admin', app.signer.dumps('*'), 'Set-Cookie': setcookie('admin', app.signer.dumps('*'),
max_age=app.MAX_AGE, path='/') max_age=app.MAX_AGE, path='/')
} }
@ -29,7 +31,7 @@ def login(app, environ, request):
def index(app, environ, request): def index(app, environ, request):
try: try:
app.unsign(request.cookies.get('session-admin', '')) app.unsign(request.cookies.get('admin', ''))
except (SignatureExpired, BadSignature): except (SignatureExpired, BadSignature):
return 301, '', {'Location': '/'} return 301, '', {'Location': '/'}

View File

@ -1,9 +1,9 @@
function initialize() { function initialize() {
$('article > footer > a').forEach(function(item) { $('div.buttons > a').forEach(function(item) {
var node = $(item).parent().parent()[0] var node = $(item).parent().parent().parent().parent()[0]
var path = node.getAttribute("data-path"); var path = node.getAttribute("data-path");
var id = node.getAttribute("data-id"); var id = node.getAttribute("data-id");

View File

@ -27,58 +27,83 @@
<%def name="make(comment)"> <%def name="make(comment)">
<article class="isso" data-path="${quote(comment.path)}" data-id="${comment.id}"> <article class="isso column grid_12" data-path="${quote(comment.path)}" data-id="${comment.id}">
<header> <div class="row">
<!-- <span class="title"><a href="${comment.path}">${comment.path}</a></span> --> <div class="column grid_9">
<span class="created">${strftime('%a %d %B %Y', gmtime(comment.created))}</span> <header>
<span class="author"> <span class="title"><a href="${comment.path}">${comment.path}</a></span>
% if comment.website: </header>
<a href="${comment.website}">${comment.author}</a>
% else:
${comment.author}
% endif
</span>
<span class="email">${comment.email}</span>
</header>
<div class="text"> <div class="text">
${app.markup.convert(comment.text)} ${app.markup.convert(comment.text)}
</div>
</div>
<div class="column grid_3 options">
<ul>
<li>${strftime('%d. %B %Y um %H:%M', gmtime(comment.created))}</li>
<li>von <em>${comment.author}</em></li>
% if comment.website:
<li><a href="${comment.website}">${comment.website}</a></li>
% endif
</ul>
<div class="row buttons">
<a href="#" class="red button column grid_1">Delete</a>
% if comment.pending:
<a href="#" class="green button column grid_1">Approve</a>
% endif
</div>
</div>
</div> </div>
<footer>
% if comment.pending:
<a href="#">Approve</a> |
% endif
<a href="#">Delete</a>
</footer>
</article> </article>
</%def> </%def>
<h1>Dashboard</h1> <div class="row pending red">
<div class="column grid_9">
<h2>Pending</h2>
</div>
<div> <div class="column grid_3">
<h2>Pending</h2> <span class="limit">
<span class="limit"> [ <a href="?${query(pendinglimit=10)}">10</a>
[ <a href="?${query(pendinglimit=10)}">10</a> | <a href="?${query(pendinglimit=20)}">20</a>
| <a href="?${query(pendinglimit=20)}">20</a> | <a href="?${query(pendinglimit=100000)}">All</a> ]
| <a href="?${query(pendinglimit=100000)}">All</a> ] </span>
</span> </div>
</div>
<div class="row">
% for comment in app.db.recent(limit=get('pendinglimit', int), mode=2): % for comment in app.db.recent(limit=get('pendinglimit', int), mode=2):
${make(comment)} ${make(comment)}
% endfor % endfor
</div> </div>
<div> <div class="row recent green">
<h2 class="recent">Recent</h2> <div class="column grid_9">
<span class="limit"> <h2>Recent</h2>
[<a href="?${query(recentlimit=10)}">10</a> </div>
| <a href="?${query(recentlimit=20)}">20</a> <div class="column grid_3">
| <a href="?${query(recentlimit=100000)}">All</a>] <span class="limit">
</span> [ <a href="?${query(recentlimit=10)}">10</a>
| <a href="?${query(recentlimit=20)}">20</a>
| <a href="?${query(recentlimit=100000)}">All</a> ]
</span>
</div>
</div>
<div class="row">
% for comment in app.db.recent(limit=get('recentlimit', int) or 20, mode=5): % for comment in app.db.recent(limit=get('recentlimit', int) or 20, mode=5):
${make(comment)} ${make(comment)}
% endfor % endfor
</div> </div>
<footer class="row">
<p><a href="https://github.com/posativ/isso">Isso</a> Ich schrei sonst!</p>
</footer>

View File

@ -8,16 +8,116 @@
</script> </script>
<link rel="stylesheet" type="text/css" href="/static/style.css" /> <link rel="stylesheet" type="text/css" href="/static/style.css" />
<style> <style>
body {
/* ================ */
/* = The 1Kb Grid = */ /* 12 columns, 60 pixels each, with 20 pixel gutter */
/* ================ */
.grid_1 { width: 60px; }
.grid_2 { width: 140px; }
.grid_3 { width: 220px; }
.grid_4 { width: 300px; }
.grid_5 { width: 380px; }
.grid_6 { width: 460px; }
.grid_7 { width: 540px; }
.grid_8 { width: 620px; }
.grid_9 { width: 700px; }
.grid_10 { width: 780px; }
.grid_11 { width: 860px; }
.grid_12 { width: 940px; }
.column {
margin: 0 10px;
overflow: hidden;
float: left;
display: inline;
}
.row {
width: 960px;
margin: 0 auto;
overflow: hidden;
}
.row .row {
margin: 0 -10px;
width: auto;
display: inline-block;
}
* {
margin: 0;
padding: 0;
}
body {
font-size: 1em; font-size: 1em;
line-height: 1.4; line-height: 1.4;
margin: 0 auto; margin: 20px 0 0 0;
width: 960px;
background: url(/static/simple-clouds.jpg) no-repeat center center fixed;
} }
body > h1 { body > h1 {
text-align: center; text-align: center;
padding: 8px 0 8px 0;
background-color: yellowgreen;
}
article {
background-color: rgb(245, 245, 245);
box-shadow: 0px 0px 4px 0px;
border-radius: 2px;
}
article header {
font-size: 1.1em;
}
article .options {
margin: 8px;
padding-bottom: 40px
}
article .text, article header {
padding: 8px 16px 8px 16px;
}
.text p {
margin-bottom: 10px;
text-align: justify;
}
.recent, .pending {
padding: 10px 40px 10px 40px;
box-shadow: 0px 0px 2px 0px;
border-radius: 4px;
margin: 2px auto 2px auto;
}
.green {
background-color: #B7D798;
}
.red {
background-color: #D79998;
}
body > footer {
border-top: 1px solid #ccc;
padding-top: 8px;
text-align: center;
}
.button {
padding-top: 10px;
padding: 5px;
border: 1px solid #666;
color: #000;
text-decoration: none;
}
.buttons {
padding-top: 10px;
} }
<%block name="style" /> <%block name="style" />

View File

@ -9,7 +9,8 @@
#login { #login {
margin: 280px 270px 0 270px; margin: 280px 270px 0 270px;
padding: 30px; padding: 30px;
background-color: #DDD; background-color: rgb(245, 245, 245);
box-shadow: 0px 0px 4px 0px;
border-radius: 8px; border-radius: 8px;
text-align: center; text-align: center;
} }

View File

@ -10,6 +10,10 @@ import tempfile
import urlparse import urlparse
import mimetypes import mimetypes
from time import gmtime, mktime, strftime, strptime, timezone
from email.utils import parsedate
from os.path import join, dirname, abspath
from urllib import quote from urllib import quote
from Cookie import SimpleCookie from Cookie import SimpleCookie
from SocketServer import ThreadingMixIn from SocketServer import ThreadingMixIn
@ -100,11 +104,11 @@ class Rule(str):
return kwargs return kwargs
def sendfile(filename, root): def sendfile(filename, root, environ):
headers = {} headers = {}
root = os.path.abspath(root) + os.sep root = abspath(root) + os.sep
filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) filename = abspath(join(root, filename.strip('/\\')))
if not filename.startswith(root): if not filename.startswith(root):
return 403, '', headers return 403, '', headers
@ -115,10 +119,27 @@ def sendfile(filename, root):
stats = os.stat(filename) stats = os.stat(filename)
headers['Content-Length'] = str(stats.st_size) headers['Content-Length'] = str(stats.st_size)
headers['Last-Modified'] = strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime))
ims = environ.get('HTTP_IF_MODIFIED_SINCE')
if ims:
ims = parsedate(ims)
if ims is not None and mktime(ims) - timezone >= stats.st_mtime:
headers['Date'] = strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime())
return 304, '', headers
return 200, io.open(filename, 'rb'), headers return 200, io.open(filename, 'rb'), headers
def static(app, environ, request, directory, path):
try:
return sendfile(path, join(dirname(__file__), directory), environ)
except (OSError, IOError):
return 404, '', {}
def setcookie(name, value, **kwargs): def setcookie(name, value, **kwargs):
return '; '.join([quote(name, '') + '=' + quote(value, '')] + return '; '.join([quote(name, '') + '=' + quote(value, '')] +
[k.replace('_', '-') + '=' + str(v) for k, v in kwargs.iteritems()]) [k.replace('_', '-') + '=' + str(v) for k, v in kwargs.iteritems()])

View File

@ -12,9 +12,10 @@ version = re.search("__version__ = '([^']+)'",
setup( setup(
name='isso', name='isso',
version=version, version=version,
author='posativ', author='Martin Zimmermann',
author_email='info@posativ.org', author_email='info@posativ.org',
packages=find_packages(), packages=find_packages(),
include_package_data=True,
zip_safe=True, zip_safe=True,
url='https://github.com/posativ/isso/', url='https://github.com/posativ/isso/',
license='BSD revised', license='BSD revised',