I went ahead and started working out how to get my todo list online. I started off pretty simple and ended up with a relatively nice system. The basic idea is that I can push my org files to my webserver and edit them. Likewise, I can pull from the server. It started with some simple paver scripts that uploaded the files and quickly became an actual application.
import os
from mercurial import commands, ui, hg
from paver.easy import *
import subprocess
IONROCK_HG = 'ssh://eric@ionrock.org/path/to/todos/'
REMOTE_TODO = IONROCK_HG # '/local/dev/path/to/todos'
@task
def server():
import cherrypy
cherrypy.tree.graft(TodoServer(base_url='/'), '/')
cherrypy.quickstart()
@task
def create_repo():
cmd = subprocess.call("fab create_repo:hosts='ionrock.org'", shell=True)
@task
def commit():
conf = ui.ui()
user = conf.username()
repo = hg.repository(conf, '.')
files = [f for f in os.listdir('.') if f.endswith('.org')]
commands.add(conf, repo, *files)
commands.commit(conf, repo, addremove=True, message='Syncing org files')
commands.push(conf, repo, REMOTE_TODO)
@task
@needs('commit')
def pull():
conf = ui.ui()
user = conf.username()
repo = hg.repository(conf, '.')
commands.pull(conf, repo, REMOTE_TODO)
commands.update(conf, repo)
@task
@needs('commit')
def update():
cmd = subprocess.call("fab update_todos:hosts='ionrock.org'", shell=True)
from fabric import run
def update_todos():
run('cd /home/eric/htdocs/todo && hg up')
def create_repo():
run('cd /home/eric/htdocs/todo && hg init')
import os
import re
import posixpath as path
import difflib
from selector import Selector
from webob import Response, Request
from webob.exc import *
from mercurial import commands, ui, hg
import datetime
class TodoFile(object):
def __init__(self, fn):
self.fn = fn
self.html_diff = difflib.HtmlDiff()
self.diff = difflib.Differ()
self.matcher = difflib.SequenceMatcher()
lines = [l for l in open(fn, 'r')]
self.matcher.set_seq2(lines)
def _hg(self):
conf = ui.ui()
user = conf.username()
repo = hg.repository(os.path.dirname(self.fn))
return conf, repo, user
def __str__(self):
return ''.join(self.read())
def read(self):
return [l for l in open(self.fn, 'r')]
def write(self, new):
f = open(self.fn, 'w')
clean = re.sub('\r', '', new)
f.write(new)
f.close()
conf, repo, user = self._hg()
date = datetime.datetime.now().strftime('%m-%d-%y %H:%M')
commands.commit(conf, repo, message='Web write on %s' % date)
def is_different(self, new):
self.matcher.set_seqs(new.split('\n'), self.read())
def diff_txt(self, new):
return list(difflib.context_diff(new.split('\n'), self.read()))
def diff_html(self, new):
return self.html_diff.make_file(self.read(), new.split('\n'))
class TodoStore(object):
def __init__(self, directory):
self.dir = os.path.abspath(directory)
def get_todo(self, name):
for fn in os.listdir(self.dir):
if fn.endswith('.org') and (fn[:-4] == name):
return TodoFile(os.path.join(self.dir, fn))
return false
def all(self):
return [fn[:-4] for fn in os.listdir(self.dir) if fn.endswith('.org')]
class Auth(object):
def __init__(self, creds, login_url, success_url=None):
self.login_url = login_url
self.success_url = success_url
self.creds = creds
def __call__(self, f):
def func(env, sr):
sess = env['beaker.session']
if sess.get('auth.user'):
return f(env, sr)
req = Request(env)
sess['auth.after_login_url'] = req.url
sess.save()
return HTTPSeeOther(location=self.login_url)(env, sr)
return func
def login(self, env, sr):
res = Response()
sess = env['beaker.session']
flash = sess.get('flash', '')
if flash:
sess['flash'] = ''
sess.save()
res.write('''<div>%s</div>
<form action="%s" method="post">
<label for="username">Username</label>
<input type="text" name="username" value=""><br />
<label for="password">Password</label>
<input type="password" name="password" value=""><br />
<input type="submit" value="login" />
</form>''' % (flash, self.login_url))
return res(env, sr)
def handle_login(self, env, sr):
req = Request(env)
post = req.POST
sess = env['beaker.session']
if post.get('username') and post.get('password'):
if self.creds.get(post['username']):
if self.creds[post['username']] == post['password']:
sess['auth.user'] = post['username']
url = sess.get('auth.after_login_url', self.success_url)
sess.save()
return HTTPSeeOther(location=url)(env, sr)
sess['flash'] = 'Error logging in.'
sess.save()
return HTTPSeeOther(location=self.login_url)(env, sr)
class TodoServer(object):
def __init__(self, **config):
self.conf = {
'todo_dir': os.path.dirname(os.path.abspath(__file__)),
}
self.conf.update(config or {})
self.auth = Auth(self.conf.get('creds', {}),
self.url('login'),
self.url())
self.store = TodoStore(self.conf['todo_dir'])
self.router = Selector([
('[/]', {'GET': self.listing}),
('/login[/]', {
'GET': self.auth.login,
'POST': self.auth.handle_login
}),
('/{name}/edit[/]', {
'GET': self.edit,
'POST': self.auth(self.update)
}),
('/{name}[/]', {'GET': self.read}),
])
def url(self, extras=None):
extras = extras or ''
if isinstance(extras, list):
extras = '/'.join(extras)
return path.join(self.conf['base_url'], extras)
def _header(self):
return '''<html><head>
<title>org todo server</title>
<style type="text/css">
body {
font-size: 2em; font-family: sans-serif;
}
</style>
'''
def _footer(self):
return '''</body></html>'''
def edit(self, env, sr):
res = Response()
req = Request(env)
name = req.urlvars['name']
td = self.store.get_todo(name)
res.write(self._header())
res.write('''
<form action="%s" method="post">
<input type="submit" name="submit" value="save" /><br />
<textarea rows="50" cols="80" name="new_body">%s</textarea>
</form>
''' % (self.url('%s/edit' % name), str(td)))
res.write(self._footer())
return res(env, sr)
def update(self, env, sr):
req = Request(env)
name = req.urlvars['name']
new_body = req.POST['new_body']
todo = self.store.get_todo(name)
todo.write(new_body.strip())
location = self.url('%s' % name)
return HTTPSeeOther(location=location)(env, sr)
def read(self, env, sr):
res = Response()
req = Request(env)
name = req.urlvars['name']
res.write(self._header())
res.write('''
Home | Edit
<hr />
<pre>''' % (self.url(), self.url('%s/edit' % name)))
td = self.store.get_todo(name)
res.write(str(td))
res.write('</pre>')
res.write(self._footer())
return res(env, sr)
def listing(self, env, sr):
res = Response()
res.write(self._header())
res.write('<ul>\n')
for f in self.store.all():
res.write('<li>%s</li>\n' % (self.url(f), f))
res.write('</ul>\n')
res.write(self._footer())
return res(env, sr)
def __call__(self, env, sr):
return self.router(env, sr)