better admin 'theme', 304 Not Modified support and minor improvements
This commit is contained in:
parent
5968a977ab
commit
65c2fce636
2
README
2
README
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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': '/'}
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
@ -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;
|
||||
}
|
||||
|
27
isso/wsgi.py
27
isso/wsgi.py
@ -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()])
|
||||
|
3
setup.py
3
setup.py
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user