From 8b8eb8d7b18fddca731e376728b5a4e8a5158396 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Dec 2008 18:48:04 -0800 Subject: [PATCH] IGN:... --- src/calibre/trac/donations/server.py | 435 +++++++++++++++++++++++++++ 1 file changed, 435 insertions(+) create mode 100644 src/calibre/trac/donations/server.py 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 = '\n\n%s
CountryContribution
'%('\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 + + + + + + + + + + + +
+ + + + + + +
+

Donations to date

+ %(todate)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