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

View File

@ -54,7 +54,8 @@ class Isso(object):
MAX_AGE = 15 * 60
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',
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]]), [
# moderation panel
('/', admin.login, ['GET', 'POST']),
('/admin/', admin.index, ['GET', 'POST']),
('/', admin.login, ['HEAD', '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
# actually unnecessary. PEP 333 aka WSGI always unquotes PATH_INFO.
('/1.0/<(.+?):path>/new', comment.create, 'POST'),
('/1.0/<(.+?):path>/<(int):id>', comment.get, 'GET'),
('/1.0/<(.+?):path>/new', comment.create,'POST'),
('/1.0/<(.+?):path>/<(int):id>', comment.get, ['HEAD', 'GET']),
('/1.0/<(.+?):path>/<(int):id>', comment.modify, ['PUT', 'DELETE']),
('/1.0/<(.+?):path>/<(int):id>/approve', comment.approve, 'PUT'),
@ -117,11 +121,11 @@ class Isso(object):
if code == 404:
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):
try:
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):
pass
@ -181,7 +185,7 @@ def main():
print 'isso', __version__
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) < 2:

View File

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

View File

@ -1,9 +1,9 @@
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 id = node.getAttribute("data-id");

View File

@ -27,58 +27,83 @@
<%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}">
<div class="row">
<div class="column grid_9">
<header>
<!-- <span class="title"><a href="${comment.path}">${comment.path}</a></span> -->
<span class="created">${strftime('%a %d %B %Y', gmtime(comment.created))}</span>
<span class="author">
% if comment.website:
<a href="${comment.website}">${comment.author}</a>
% else:
${comment.author}
% endif
</span>
<span class="email">${comment.email}</span>
<span class="title"><a href="${comment.path}">${comment.path}</a></span>
</header>
<div class="text">
${app.markup.convert(comment.text)}
</div>
<footer>
% if comment.pending:
<a href="#">Approve</a> |
</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
<a href="#">Delete</a>
</footer>
</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>
</article>
</%def>
<h1>Dashboard</h1>
<div>
<div class="row pending red">
<div class="column grid_9">
<h2>Pending</h2>
</div>
<div class="column grid_3">
<span class="limit">
[ <a href="?${query(pendinglimit=10)}">10</a>
| <a href="?${query(pendinglimit=20)}">20</a>
| <a href="?${query(pendinglimit=100000)}">All</a> ]
</span>
</div>
</div>
<div class="row">
% for comment in app.db.recent(limit=get('pendinglimit', int), mode=2):
${make(comment)}
% endfor
</div>
<div>
<h2 class="recent">Recent</h2>
<div class="row recent green">
<div class="column grid_9">
<h2>Recent</h2>
</div>
<div class="column grid_3">
<span class="limit">
[<a href="?${query(recentlimit=10)}">10</a>
[ <a href="?${query(recentlimit=10)}">10</a>
| <a href="?${query(recentlimit=20)}">20</a>
| <a href="?${query(recentlimit=100000)}">All</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):
${make(comment)}
% endfor
</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>
<link rel="stylesheet" type="text/css" href="/static/style.css" />
<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;
line-height: 1.4;
margin: 0 auto;
width: 960px;
margin: 20px 0 0 0;
background: url(/static/simple-clouds.jpg) no-repeat center center fixed;
}
body > h1 {
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" />

View File

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

View File

@ -10,6 +10,10 @@ import tempfile
import urlparse
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 Cookie import SimpleCookie
from SocketServer import ThreadingMixIn
@ -100,11 +104,11 @@ class Rule(str):
return kwargs
def sendfile(filename, root):
def sendfile(filename, root, environ):
headers = {}
root = os.path.abspath(root) + os.sep
filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
root = abspath(root) + os.sep
filename = abspath(join(root, filename.strip('/\\')))
if not filename.startswith(root):
return 403, '', headers
@ -115,10 +119,27 @@ def sendfile(filename, root):
stats = os.stat(filename)
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
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):
return '; '.join([quote(name, '') + '=' + quote(value, '')] +
[k.replace('_', '-') + '=' + str(v) for k, v in kwargs.iteritems()])

View File

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