mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
IGN:...
This commit is contained in:
parent
45afc46f33
commit
8b8eb8d7b1
435
src/calibre/trac/donations/server.py
Normal file
435
src/calibre/trac/donations/server.py
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
'''
|
||||||
|
Keep track of donations to calibre.
|
||||||
|
'''
|
||||||
|
import sys, cStringIO, textwrap, traceback, re
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from math import sqrt
|
||||||
|
|
||||||
|
import cherrypy
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
def range_for_month(year, month):
|
||||||
|
ty, tm = date.today().year, date.today().month
|
||||||
|
min = date(year=year, month=month, day=1)
|
||||||
|
x = date.today().day if ty == year and tm == month else 31
|
||||||
|
while x > 1:
|
||||||
|
try:
|
||||||
|
max = min.replace(day=x)
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
x -= 1
|
||||||
|
return min, max
|
||||||
|
|
||||||
|
def range_for_year(year):
|
||||||
|
return date(year=year, month=1, day=1), date(year=year, month=12, day=31)
|
||||||
|
|
||||||
|
|
||||||
|
def rationalize_country(country):
|
||||||
|
if re.match('(?i)(US|USA|America)', country):
|
||||||
|
country = 'USA'
|
||||||
|
elif re.match('(?i)(UK|Britain|england)', country):
|
||||||
|
country = 'UK'
|
||||||
|
elif re.match('(?i)italy', country):
|
||||||
|
country = 'Italy'
|
||||||
|
elif re.match('(?i)germany', country):
|
||||||
|
country = 'Germany'
|
||||||
|
elif re.match('(?i)france', country):
|
||||||
|
country = 'France'
|
||||||
|
elif re.match('(?i)ireland', country):
|
||||||
|
country = 'Ireland'
|
||||||
|
elif re.match('(?i)norway', country):
|
||||||
|
country = 'Norway'
|
||||||
|
elif re.match('(?i)canada', country):
|
||||||
|
country = 'Canada'
|
||||||
|
elif re.match(r'(?i)new\s*zealand', country):
|
||||||
|
country = 'New Zealand'
|
||||||
|
elif re.match('(?i)jamaica', country):
|
||||||
|
country = 'Jamaica'
|
||||||
|
elif re.match('(?i)australia', country):
|
||||||
|
country = 'Australia'
|
||||||
|
elif re.match('(?i)Netherlands', country):
|
||||||
|
country = 'Netherlands'
|
||||||
|
elif re.match('(?i)spain', country):
|
||||||
|
country = 'Spain'
|
||||||
|
elif re.match('(?i)colombia', country):
|
||||||
|
country = 'Colombia'
|
||||||
|
return country
|
||||||
|
|
||||||
|
class Record(object):
|
||||||
|
|
||||||
|
def __init__(self, email, country, amount, date, name):
|
||||||
|
self.email = email
|
||||||
|
self.country = country
|
||||||
|
self.amount = amount
|
||||||
|
self.date = date
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '<donation email="%s" country="%s" amount="%.2f" date="%s" %s />'%\
|
||||||
|
(self.email, self.country, self.amount, self.date.isoformat(), 'name="%s"'%self.name if self.name else '')
|
||||||
|
|
||||||
|
class Country(list):
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
list.__init__(self)
|
||||||
|
self.name = name
|
||||||
|
self.total = 0.
|
||||||
|
self.percent = 0.
|
||||||
|
|
||||||
|
def append(self, r):
|
||||||
|
self.total += r.amount
|
||||||
|
list.append(self, r)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name + ': %.2f%%'%self.percent
|
||||||
|
|
||||||
|
def __cmp__(self, other):
|
||||||
|
return cmp(self.total, other.total)
|
||||||
|
|
||||||
|
|
||||||
|
class Stats:
|
||||||
|
|
||||||
|
def get_deviation(self, amounts):
|
||||||
|
l = float(len(amounts))
|
||||||
|
mean = sum(amounts)/l
|
||||||
|
return sqrt( sum([i**2 for i in amounts])/l - mean**2 )
|
||||||
|
|
||||||
|
def __init__(self, records):
|
||||||
|
self.total = sum([r.amount for r in records])
|
||||||
|
self.days = {}
|
||||||
|
l, rg = date.max, date.min
|
||||||
|
self.totals = []
|
||||||
|
for r in records:
|
||||||
|
self.totals.append(r.amount)
|
||||||
|
l, rg = min(l, r.date), max(rg, r.date)
|
||||||
|
if r.date not in self.days.keys():
|
||||||
|
self.days[r.date] = []
|
||||||
|
self.days[r.date].append(r)
|
||||||
|
|
||||||
|
self.min, self.max = l, rg
|
||||||
|
self.period = self.max - self.min
|
||||||
|
daily_totals = []
|
||||||
|
day = self.min
|
||||||
|
while day <= self.max:
|
||||||
|
x = self.days.get(day, [])
|
||||||
|
daily_totals.append(sum([y.amount for y in x]))
|
||||||
|
day += timedelta(days=1)
|
||||||
|
self.daily_average = self.total/len(daily_totals) if len(daily_totals) else 0.
|
||||||
|
self.daily_deviation = self.get_deviation(daily_totals)
|
||||||
|
self.average = self.total/len(records) if len(records) else 0.
|
||||||
|
self.average_deviation = self.get_deviation(self.totals)
|
||||||
|
self.countries = {}
|
||||||
|
self.daily_totals = daily_totals
|
||||||
|
for r in records:
|
||||||
|
if r.country not in self.countries.keys():
|
||||||
|
self.countries[r.country] = Country(r.country)
|
||||||
|
self.countries[r.country].append(r)
|
||||||
|
for country in self.countries.values():
|
||||||
|
country.percent = (100 * country.total/self.total) if self.total else 0.
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
buf = cStringIO.StringIO()
|
||||||
|
print >>buf, '\tTotal: %.2f'%self.total
|
||||||
|
print >>buf, '\tDaily Average: %.2f'%self.daily_average
|
||||||
|
print >>buf, '\tAverage contribution: %.2f'%self.average
|
||||||
|
print >>buf, '\tCountry breakup:'
|
||||||
|
for c in self.countries.values():
|
||||||
|
print >>buf, '\t\t', c
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
def to_html(self, num_of_countries=sys.maxint):
|
||||||
|
countries = sorted(self.countries.values(), cmp=cmp, reverse=True)[:num_of_countries]
|
||||||
|
crows = ['<tr><td>%s</td><td class="country_percent">%.2f %%</td></tr>'%(c.name, c.percent) for c in countries]
|
||||||
|
ctable = '<table>\n<tr><th>Country</th><th>Contribution</th></tr>\n%s</table>'%('\n'.join(crows))
|
||||||
|
if num_of_countries < sys.maxint:
|
||||||
|
ctable = '<p>Top %d countries</p>'%num_of_countries + ctable
|
||||||
|
return textwrap.dedent('''
|
||||||
|
<div class="stats">
|
||||||
|
<p style="font-weight: bold">Donations in %(period)d days [%(min)s - %(max)s]:</p>
|
||||||
|
<table style="border-left: 4em">
|
||||||
|
<tr><td>Total</td><td class="money">$%(total).2f (%(num)d)</td></tr>
|
||||||
|
<tr><td>Daily average</td><td class="money">$%(da).2f ± %(dd).2f</td></tr>
|
||||||
|
<tr><td>Average contribution</td><td class="money">$%(ac).2f ± %(ad).2f</td></tr>
|
||||||
|
<tr><td>Donors per day</td><td class="money">%(dpd).2f</td></tr>
|
||||||
|
</table>
|
||||||
|
<br />
|
||||||
|
%(ctable)s
|
||||||
|
</div>
|
||||||
|
''')%dict(total=self.total, da=self.daily_average, ac=self.average,
|
||||||
|
ctable=ctable, period=self.period.days, num=len(self.totals),
|
||||||
|
dd=self.daily_deviation, ad=self.average_deviation,
|
||||||
|
dpd=len(self.totals)/float(self.period.days),
|
||||||
|
min=self.min.isoformat(), max=self.max.isoformat())
|
||||||
|
|
||||||
|
|
||||||
|
def expose(func):
|
||||||
|
|
||||||
|
def do(self, *args, **kwargs):
|
||||||
|
dict.update(cherrypy.response.headers, {'Server':'Donations_server/1.0'})
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return cherrypy.expose(do)
|
||||||
|
|
||||||
|
class Server(object):
|
||||||
|
|
||||||
|
def __init__(self, apache=False, root='/', data_file='/tmp/donations.xml'):
|
||||||
|
self.apache = apache
|
||||||
|
self.document_root = root
|
||||||
|
self.tree = etree.parse(data_file)
|
||||||
|
self.root = self.tree.getroot()
|
||||||
|
self.read_records()
|
||||||
|
|
||||||
|
def read_records(self):
|
||||||
|
self.records = []
|
||||||
|
min_date, max_date = date.today(), date.fromordinal(1)
|
||||||
|
for x in self.root.xpath('//donation'):
|
||||||
|
d = list(map(int, x.get('date').split('-')))
|
||||||
|
d = date(*d)
|
||||||
|
self.records.append(Record(x.get('email'), x.get('country'), float(x.get('amount')), d, x.get('name')))
|
||||||
|
min_date = min(min_date, d)
|
||||||
|
max_date = max(max_date, d)
|
||||||
|
self.earliest, self.latest = min_date, max_date
|
||||||
|
|
||||||
|
def get_slice(self, start_date, end_date):
|
||||||
|
stats = Stats([r for r in self.records if r.date >= start_date and r.date <= end_date])
|
||||||
|
if start_date > date.min and end_date < date.max:
|
||||||
|
stats.period = end_date - start_date
|
||||||
|
stats.period += timedelta(days=1)
|
||||||
|
stats.min = start_date
|
||||||
|
stats.max = end_date
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def month(self, year, month):
|
||||||
|
return self.get_slice(*range_for_month(year, month))
|
||||||
|
|
||||||
|
def year(self, year):
|
||||||
|
return self.get_slice(*range_for_year(year))
|
||||||
|
|
||||||
|
def range_to_date(self, raw):
|
||||||
|
return date(*map(int, raw.split('-')))
|
||||||
|
|
||||||
|
def build_page(self, period_type, data):
|
||||||
|
month = date.today().month
|
||||||
|
year = date.today().year
|
||||||
|
mm = data[1] if period_type == 'month' else month
|
||||||
|
my = data[0] if period_type == 'month' else year
|
||||||
|
yy = data if period_type == 'year' else year
|
||||||
|
rl = data[0] if period_type == 'range' else ''
|
||||||
|
rr = data[1] if period_type == 'range' else ''
|
||||||
|
|
||||||
|
def build_month_list(current):
|
||||||
|
months = []
|
||||||
|
for i in range(1, 13):
|
||||||
|
month = date(2000, i, 1).strftime('%b')
|
||||||
|
sel = 'selected="selected"' if i == current else ''
|
||||||
|
months.append('<option value="%d" %s>%s</option>'%(i, sel, month))
|
||||||
|
return months
|
||||||
|
|
||||||
|
def build_year_list(current):
|
||||||
|
all_years = sorted(range(self.earliest.year, self.latest.year+1, 1))
|
||||||
|
if current not in all_years:
|
||||||
|
current = all_years[0]
|
||||||
|
years = []
|
||||||
|
for year in all_years:
|
||||||
|
sel = 'selected="selected"' if year == current else ''
|
||||||
|
years.append('<option value="%d" %s>%d</option>'%(year, sel, year))
|
||||||
|
return years
|
||||||
|
|
||||||
|
mmlist = '<select name="month_month">\n%s</select>'%('\n'.join(build_month_list(mm)))
|
||||||
|
mylist = '<select name="month_year">\n%s</select>'%('\n'.join(build_year_list(my)))
|
||||||
|
yylist = '<select name="year_year">\n%s</select>'%('\n'.join(build_year_list(yy)))
|
||||||
|
|
||||||
|
if period_type == 'month':
|
||||||
|
range_stats = range_for_month(my, mm)
|
||||||
|
elif period_type == 'year':
|
||||||
|
range_stats = range_for_year(yy)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
range_stats = list(map(self.range_to_date, (rl, rr)))
|
||||||
|
err = None
|
||||||
|
except:
|
||||||
|
range_stats = None
|
||||||
|
err = traceback.format_exc()
|
||||||
|
if range_stats is None:
|
||||||
|
range_stats = '<pre>Invalid input:\n%s</pre>'%err
|
||||||
|
else:
|
||||||
|
range_stats = self.get_slice(*range_stats).to_html(num_of_countries=10)
|
||||||
|
|
||||||
|
return textwrap.dedent('''\
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" version="XHTML 1.1" xml:lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Calibre donations</title>
|
||||||
|
<link rel="icon" href="http://calibre.kovidgoyal.net/chrome/site/favicon.ico" type="image/x-icon" />
|
||||||
|
<style type="text/css">
|
||||||
|
body { background-color: white }
|
||||||
|
.country_percent { text-align: right; font-family: monospace; }
|
||||||
|
.money { text-align: right; font-family: monospace; padding-left:2em;}
|
||||||
|
.period_box { padding-left: 60px; border-bottom: 10px; }
|
||||||
|
#banner {font-size: xx-large; font-family: cursive; text-align: center}
|
||||||
|
#stats_container td { vertical-align: top }
|
||||||
|
</style>
|
||||||
|
<script type="text/javascript">
|
||||||
|
String.prototype.trim = function() {
|
||||||
|
return this.replace(/^\s+|\s+$/g,"");
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_date(date) {
|
||||||
|
var valid_format = /\d{4}-\d{1,2}-\d{1,2}/;
|
||||||
|
if (!valid_format.test(date)) return false;
|
||||||
|
var yearfield = date.split('-')[0];
|
||||||
|
var monthfield = date.split('-')[1];
|
||||||
|
var dayfield = date.split('-')[2];
|
||||||
|
var dayobj = new Date(yearfield, monthfield-1, dayfield)
|
||||||
|
if ((dayobj.getMonth()+1!=monthfield)||(dayobj.getDate()!=dayfield)||(dayobj.getFullYear()!=yearfield)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_period_form(form) {
|
||||||
|
if (form.period_type[2].checked) {
|
||||||
|
if (!test_date(form.range_left.value)) {
|
||||||
|
form.range_left.focus();
|
||||||
|
alert("Left Range date invalid!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!test_date(form.range_right.value)) {
|
||||||
|
form.range_right.focus();
|
||||||
|
alert("Right Range date invalid!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_empty(val) {
|
||||||
|
return val.trim().length == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_add_form(form) {
|
||||||
|
var test_amount = /[\.0-9]+/;
|
||||||
|
if (is_empty(form.email.value)) {
|
||||||
|
form.email.focus();
|
||||||
|
alert("Email must be filled!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (is_empty(form.country.value)) {
|
||||||
|
form.country.focus();
|
||||||
|
alert("Country must be filled!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!test_amount.test(form.amount.value)) {
|
||||||
|
form.amount.focus();
|
||||||
|
alert("Amount " + form.amount.value + " is not a valid number!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!test_date(form.date.value)) {
|
||||||
|
form.date.focus();
|
||||||
|
alert("Date " + form.date.value +" is invalid!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table id="banner" style="width: 100%%">
|
||||||
|
<tr>
|
||||||
|
<td style="text-align:left; width:150px"><a style="border:0pt" href="http://calibre.kovidgoyal.net"><img style="vertical-align: middle;border:0pt" alt="calibre" src="http://calibre.kovidgoyal.net/chrome/site/calibre_banner.png" /></a></td>
|
||||||
|
<td>Calibre donations</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<hr />
|
||||||
|
<table id="stats_container" style="width:100%%">
|
||||||
|
<tr>
|
||||||
|
<td id="left">
|
||||||
|
<h3>Donations to date</h3>
|
||||||
|
%(todate)s
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td id="right">
|
||||||
|
<h3>Donations in period</h3>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Choose a period</legend>
|
||||||
|
<form method="post" action="%(root)sshow" onsubmit="return check_period_form(this);">
|
||||||
|
<input type="radio" name="period_type" value="month" %(mc)s />
|
||||||
|
Month: %(month_month)s %(month_year)s
|
||||||
|
<br /><br />
|
||||||
|
<input type="radio" name="period_type" value="year" %(yc)s />
|
||||||
|
Year: %(year_year)s
|
||||||
|
<br /><br />
|
||||||
|
<input type="radio" name="period_type" value="range" %(rc)s />
|
||||||
|
Range (YYYY-MM-DD): <input size="10" maxlength="10" type="text" name="range_left" value="%(rl)s" /> to <input size="10" maxlength="10" type="text" name="range_right" value="%(rr)s"/>
|
||||||
|
<br /><br />
|
||||||
|
<input type="submit" value="Update" />
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
|
%(range_stats)s
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<hr />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
''')%dict(
|
||||||
|
todate=self.get_slice(self.earliest, self.latest).to_html(),
|
||||||
|
mc = 'checked="checked"' if period_type=="month" else '',
|
||||||
|
yc = 'checked="checked"' if period_type=="year" else '',
|
||||||
|
rc = 'checked="checked"' if period_type=="range" else '',
|
||||||
|
month_month=mmlist, month_year=mylist, year_year=yylist,
|
||||||
|
rl=rl, rr=rr, range_stats=range_stats, root=self.document_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
@expose
|
||||||
|
def index(self):
|
||||||
|
month = date.today().month
|
||||||
|
year = date.today().year
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'application/xhtml+xml'
|
||||||
|
return self.build_page('month', (year, month))
|
||||||
|
|
||||||
|
@expose
|
||||||
|
def show(self, period_type='month', month_month='', month_year='',
|
||||||
|
year_year='', range_left='', range_right=''):
|
||||||
|
if period_type == 'month':
|
||||||
|
mm = int(month_month) if month_month else date.today().month
|
||||||
|
my = int(month_year) if month_year else date.today().year
|
||||||
|
data = (my, mm)
|
||||||
|
elif period_type == 'year':
|
||||||
|
data = int(year_year) if year_year else date.today().year
|
||||||
|
else:
|
||||||
|
data = (range_left, range_right)
|
||||||
|
cherrypy.response.headers['Content-Type'] = 'application/xhtml+xml'
|
||||||
|
return self.build_page(period_type, data)
|
||||||
|
|
||||||
|
def config():
|
||||||
|
config = {
|
||||||
|
'global': {
|
||||||
|
'tools.gzip.on' : True,
|
||||||
|
'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/xml', 'text/javascript', 'text/css', 'application/xhtml+xml'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
|
||||||
|
def apache_start():
|
||||||
|
cherrypy.config.update({
|
||||||
|
'log.screen' : False,
|
||||||
|
#'log.error_file' : '/tmp/donations.log',
|
||||||
|
'environment' : 'production',
|
||||||
|
'show_tracebacks' : False,
|
||||||
|
})
|
||||||
|
cherrypy.tree.mount(Server(apache=True, root='/donations/', data_file='/var/www/calibre.kovidgoyal.net/donations.xml'),
|
||||||
|
'/donations', config=config())
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=sys.argv):
|
||||||
|
server = Server()
|
||||||
|
cherrypy.quickstart(server, config=config())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
Loading…
x
Reference in New Issue
Block a user