diff --git a/src/calibre/trac/donations/server.py b/src/calibre/trac/donations/server.py
new file mode 100644
index 0000000000..5da2efa058
--- /dev/null
+++ b/src/calibre/trac/donations/server.py
@@ -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 ''%\
+ (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 = ['
%s | %.2f %% |
'%(c.name, c.percent) for c in countries]
+ ctable = '\nCountry | Contribution |
\n%s
'%('\n'.join(crows))
+ if num_of_countries < sys.maxint:
+ ctable = 'Top %d countries
'%num_of_countries + ctable
+ return textwrap.dedent('''
+
+
Donations in %(period)d days [%(min)s - %(max)s]:
+
+ Total | $%(total).2f (%(num)d) |
+ Daily average | $%(da).2f ± %(dd).2f |
+ Average contribution | $%(ac).2f ± %(ad).2f |
+ Donors per day | %(dpd).2f |
+
+
+ %(ctable)s
+
+ ''')%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(''%(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(''%(year, sel, year))
+ return years
+
+ mmlist = ''%('\n'.join(build_month_list(mm)))
+ mylist = ''%('\n'.join(build_year_list(my)))
+ yylist = ''%('\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 = 'Invalid input:\n%s
'%err
+ else:
+ range_stats = self.get_slice(*range_stats).to_html(num_of_countries=10)
+
+ return textwrap.dedent('''\
+
+
+
+
+ Calibre donations
+
+
+
+
+
+
+
+  |
+ Calibre donations |
+
+
+
+
+
+
+ Donations to date
+ %(todate)s
+ |
+
+
+ Donations in period
+
+ %(range_stats)s
+ |
+
+
+
+
+
+ ''')%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())
\ No newline at end of file