2012-02-28 11:37:03 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
# lunchuman.wsgi: a WSGI script for managing a simple lunch club
|
|
|
|
# Copyright (C) 2012 Niels G. W. Serup
|
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful, but
|
|
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
# Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public
|
|
|
|
# License along with this program. If not, see
|
|
|
|
# <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
## Version: 0.1.0
|
|
|
|
## Maintainer: Niels G. W. Serup <ns@metanohi.name>
|
|
|
|
## Temporary website: http://datalosjen.metanohi.name/static/source/lunchuman
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
import bottle
|
|
|
|
from bottle import route, abort, error, request, redirect
|
|
|
|
import sqlite3
|
2012-03-22 16:21:56 +01:00
|
|
|
import datetime
|
2012-02-28 11:37:03 +01:00
|
|
|
import threading
|
|
|
|
import itertools
|
|
|
|
import atexit
|
|
|
|
import pickle
|
|
|
|
import configparser
|
|
|
|
import locale
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
_preferred_encoding = 'utf-8' # locale.getpreferredencoding() # fail
|
|
|
|
_absfile = os.path.abspath(__file__)
|
|
|
|
_filedir = os.path.dirname(_absfile)
|
|
|
|
_configfile_name = 'lunchuman.config'
|
|
|
|
|
|
|
|
def start_bottle():
|
|
|
|
global application
|
|
|
|
application = bottle.default_app()
|
|
|
|
|
|
|
|
def block():
|
|
|
|
threading.Event().wait()
|
|
|
|
|
|
|
|
with open(os.path.join(_filedir, '.dynamicsettings'), 'rb') as f:
|
|
|
|
dynamic_settings = pickle.load(f)
|
|
|
|
|
|
|
|
if dynamic_settings['status'] == 'stop':
|
|
|
|
start_bottle()
|
|
|
|
block()
|
|
|
|
|
|
|
|
|
|
|
|
class LunchumanDatabase:
|
|
|
|
def __init__(self, fname):
|
|
|
|
self.dbfile = fname
|
|
|
|
|
|
|
|
def load(self):
|
|
|
|
self.conn = sqlite3.connect(self.dbfile)
|
|
|
|
self.cursor = self.conn.cursor()
|
|
|
|
self._create()
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
self.conn.close()
|
|
|
|
|
|
|
|
def _create(self):
|
|
|
|
self.cursor.execute('''
|
|
|
|
create table if not exists users (
|
|
|
|
id integer primary key autoincrement,
|
|
|
|
name text,
|
|
|
|
moneys int,
|
|
|
|
foods int
|
|
|
|
)
|
2012-03-22 16:21:56 +01:00
|
|
|
''')
|
|
|
|
self.cursor.execute('''
|
|
|
|
create table if not exists actions (
|
|
|
|
id integer primary key autoincrement,
|
|
|
|
action_when datetime,
|
|
|
|
action text
|
|
|
|
)
|
2012-02-28 11:37:03 +01:00
|
|
|
''')
|
|
|
|
self.conn.commit()
|
|
|
|
|
|
|
|
def get_users(self):
|
2012-03-06 12:56:50 +01:00
|
|
|
self.cursor.execute('select * from users order by moneys / (foods * 1.0) desc')
|
2012-02-28 11:37:03 +01:00
|
|
|
return self.cursor.fetchall()
|
|
|
|
|
|
|
|
def add_user(self, name):
|
|
|
|
self.cursor.execute('insert into users values (NULL, ?, 0, 0)', (name,))
|
2012-03-22 16:21:56 +01:00
|
|
|
self.log('new user created: {}'.format(repr(name)))
|
2012-02-28 11:37:03 +01:00
|
|
|
self.conn.commit()
|
|
|
|
|
2012-03-22 16:21:56 +01:00
|
|
|
def log(self, msg):
|
|
|
|
self.cursor.execute('insert into actions values (NULL, ?, ?)',
|
|
|
|
(datetime.datetime.utcnow(), msg))
|
|
|
|
self.conn.commit()
|
|
|
|
|
|
|
|
def get_log(self):
|
|
|
|
self.cursor.execute('select * from actions order by action_when desc')
|
|
|
|
return self.cursor.fetchall()
|
|
|
|
|
2012-02-28 11:37:03 +01:00
|
|
|
def update_user_data(self, *users):
|
2012-03-22 16:21:56 +01:00
|
|
|
log_msg = []
|
2012-02-28 11:37:03 +01:00
|
|
|
for id, new_name, moneys_incr, foods_incr in users:
|
2012-03-22 16:21:56 +01:00
|
|
|
try:
|
|
|
|
foods_incr = int(foods_incr)
|
|
|
|
except ValueError:
|
|
|
|
foods_incr = 0
|
|
|
|
try:
|
|
|
|
moneys_incr = int(moneys_incr)
|
|
|
|
except ValueError:
|
|
|
|
moneys_incr = 0
|
|
|
|
self.cursor.execute('select name from users where id=?', (id,))
|
|
|
|
old_name = self.cursor.fetchone()[0]
|
2012-02-28 11:37:03 +01:00
|
|
|
self.cursor.execute('''
|
|
|
|
update users set name=?, moneys=moneys + (?), foods=foods + (?)
|
|
|
|
where id=?
|
|
|
|
''', (new_name, moneys_incr, foods_incr, id))
|
2012-03-22 16:21:56 +01:00
|
|
|
loclog = []
|
|
|
|
if old_name != new_name:
|
2012-03-22 16:28:43 +01:00
|
|
|
loclog.append('change name to {}'.format(new_name))
|
2012-03-22 16:21:56 +01:00
|
|
|
if foods_incr > 0:
|
|
|
|
loclog.append('eat {} foods'.format(foods_incr))
|
|
|
|
elif foods_incr < 0:
|
2012-03-22 16:31:30 +01:00
|
|
|
loclog.append('uneat {} foods'.format(-foods_incr))
|
2012-03-22 16:21:56 +01:00
|
|
|
if moneys_incr > 0:
|
|
|
|
loclog.append('spend {} moneys'.format(moneys_incr))
|
|
|
|
elif moneys_incr < 0:
|
2012-03-22 16:31:30 +01:00
|
|
|
loclog.append('unspend {} moneys'.format(-moneys_incr))
|
2012-03-22 16:21:56 +01:00
|
|
|
if loclog:
|
|
|
|
log_msg.append('{} do:\n'.format(old_name)
|
|
|
|
+ ' ' + '\n '.join(loclog))
|
|
|
|
if log_msg:
|
|
|
|
self.log('\n'.join(log_msg))
|
2012-02-28 11:37:03 +01:00
|
|
|
self.conn.commit()
|
|
|
|
|
|
|
|
def add_settings_from_file(fname, settings):
|
|
|
|
cfg = configparser.SafeConfigParser(settings)
|
|
|
|
cfg.add_section('lunchuman')
|
|
|
|
cfg.read(os.path.join(_filedir, fname))
|
|
|
|
return {name: value for name, value in cfg.items('lunchuman')}
|
|
|
|
|
|
|
|
def dictpair(*ds):
|
|
|
|
return {key: val for key, val
|
|
|
|
in itertools.chain(*(d.items() for d in ds))}
|
|
|
|
|
|
|
|
def read_file(locpath):
|
|
|
|
with open(os.path.join(_filedir, locpath), 'rb') as f:
|
|
|
|
return f.read().decode(_preferred_encoding)
|
|
|
|
|
|
|
|
def get_seq_input(d, base_key):
|
|
|
|
i = 0
|
|
|
|
xs = []
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
xs.append(d['{}[{}]'.format(base_key, i)])
|
|
|
|
except KeyError:
|
|
|
|
break
|
|
|
|
i += 1
|
|
|
|
return xs
|
|
|
|
|
|
|
|
settings = add_settings_from_file(
|
|
|
|
_configfile_name, {
|
|
|
|
'name': 'Lunchuman',
|
|
|
|
'dbfile': 'lunchuman.sqlite',
|
|
|
|
'template': 'template.html',
|
|
|
|
'frontpage': 'frontpage.html'
|
|
|
|
})
|
|
|
|
|
|
|
|
_template = read_file(settings['template'])
|
|
|
|
_userdata_template = '''
|
|
|
|
<input type='hidden' name='id[{i}]' value='{id}' />
|
|
|
|
<input type='hidden' name='orig_name[{i}]' value={name} />
|
|
|
|
<tr>
|
|
|
|
<td><input type='text' name='new_name[{i}]' style='width: 140px;'
|
|
|
|
value={name} /></td>
|
|
|
|
<td>{moneys} + <input type='text' name='moneys_incr[{i}]'
|
|
|
|
style='width: 40px;' value='0' class='deleteonfocus' /></td>
|
|
|
|
<td>{foods} + <input type='text' name='foods_incr[{i}]'
|
|
|
|
style='width: 40px;' value='0' class='deleteonfocus' /></td>
|
|
|
|
<td>{goodwill}</td>
|
|
|
|
</tr>
|
|
|
|
'''
|
|
|
|
_frontpage = _template.format(**dictpair(settings, {
|
|
|
|
'content': read_file(settings['frontpage']),
|
|
|
|
'headextra': "<script type='text/javascript' \
|
|
|
|
src='/static/front.js'></script>"
|
|
|
|
}))
|
|
|
|
|
|
|
|
db = LunchumanDatabase(os.path.join(_filedir, settings['dbfile']))
|
|
|
|
#atexit.register(db.close)
|
|
|
|
db.load()
|
|
|
|
|
|
|
|
|
|
|
|
@route('/')
|
|
|
|
def frontpage():
|
|
|
|
users = db.get_users()
|
|
|
|
userdata = '\n'.join(_userdata_template.format(
|
|
|
|
id=users[i][0], name=repr(users[i][1]), moneys=users[i][2],
|
|
|
|
foods=users[i][3], i=i, goodwill=('{:.2f}'.format(
|
|
|
|
users[i][2] / users[i][3]) if users[i][3] > 0
|
|
|
|
else None))
|
|
|
|
for i in range(len(users)))
|
2012-03-22 16:21:56 +01:00
|
|
|
log = '\n\n'.join('{} UTC:\n{}'.format(*row[1:]) for row in db.get_log())
|
|
|
|
return _frontpage.format(userdata=userdata, log=log)
|
2012-02-28 11:37:03 +01:00
|
|
|
|
|
|
|
@route('/adduser', method='post')
|
|
|
|
def add_user():
|
|
|
|
db.add_user(request.forms['name'])
|
|
|
|
redirect('/')
|
|
|
|
|
|
|
|
@route('/updateuserdata', method='post')
|
|
|
|
def update_user_data():
|
2012-03-22 16:21:56 +01:00
|
|
|
try:
|
|
|
|
users = (vals[1:] for vals in
|
|
|
|
filter(lambda vals: vals[0] != vals[2] or
|
|
|
|
(vals[3] != '0' and vals[3] != '') or
|
|
|
|
(vals[4] != '0' and vals[4] != ''),
|
|
|
|
zip(*(get_seq_input(request.forms, attr)
|
|
|
|
for attr in ('orig_name', 'id', 'new_name',
|
|
|
|
'moneys_incr', 'foods_incr')))))
|
|
|
|
db.update_user_data(*users)
|
|
|
|
except Exception:
|
|
|
|
traceback.print_exc()
|
2012-02-28 11:37:03 +01:00
|
|
|
redirect('/')
|
|
|
|
|
|
|
|
|
|
|
|
start_bottle()
|