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

pull/16/head
posativ 12 years ago
parent 5968a977ab
commit 65c2fce636

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

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

@ -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': '/'}

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

@ -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: <div class="text">
${comment.author} ${app.markup.convert(comment.text)}
% endif </div>
</span>
<span class="email">${comment.email}</span> </div>
</header>
<div class="column grid_3 options">
<div class="text">
${app.markup.convert(comment.text)} <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 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>
<h2>Pending</h2>
<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 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>

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

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

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

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

Loading…
Cancel
Save