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