diff --git a/src/calibre/trac/donations/server.py b/src/calibre/trac/donations/server.py
index 2afc3adeca..3bf822b893 100644
--- a/src/calibre/trac/donations/server.py
+++ b/src/calibre/trac/donations/server.py
@@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
'''
Keep track of donations to calibre.
'''
-import sys, cStringIO, textwrap, traceback, re, os, time
+import sys, cStringIO, textwrap, traceback, re, os, time, calendar
from datetime import date, timedelta
from math import sqrt
os.environ['HOME'] = '/tmp'
@@ -15,7 +15,6 @@ matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
-
import cherrypy
from lxml import etree
@@ -33,7 +32,13 @@ def range_for_month(year, month):
def range_for_year(year):
return date(year=year, month=1, day=1), date(year=year, month=12, day=31)
-
+
+def days_in_month(year, month):
+ c = calendar.Calendar()
+ ans = 0
+ for x in c.itermonthdays(year, month):
+ if x != 0: ans += 1
+ return ans
def rationalize_country(country):
if re.match('(?i)(US|USA|America)', country):
@@ -67,14 +72,14 @@ def rationalize_country(country):
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 '')
@@ -93,7 +98,7 @@ class Country(list):
def __str__(self):
return self.name + ': %.2f%%'%self.percent
-
+
def __cmp__(self, other):
return cmp(self.total, other.total)
@@ -118,7 +123,7 @@ class Stats:
if r.date not in self.days.keys():
self.days[r.date] = []
self.days[r.date].append(r)
-
+
self.min, self.max = start, end
self.period = (self.max - self.min) + timedelta(days=1)
daily_totals = []
@@ -139,7 +144,24 @@ class Stats:
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 get_daily_averages(self):
+ month_buckets, month_order = {}, []
+ x = self.min
+ for t in self.daily_totals:
+ month = (x.year, x.month)
+ if month not in month_buckets:
+ month_buckets[month] = 0.
+ month_order.append(month)
+ month_buckets[month] += t
+ x += timedelta(days=1)
+ c = calendar.Calendar()
+ month_days = [days_in_month(*x) for x in month_order]
+ month_averages = [month_buckets[x]/float(y) for x, y in zip(month_order, month_days)]
+ return month_order, month_averages
+
+
+
def __str__(self):
buf = cStringIO.StringIO()
print >>buf, '\tTotal: %.2f'%self.total
@@ -149,7 +171,7 @@ class Stats:
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]
@@ -168,32 +190,48 @@ class Stats:
%(ctable)s
- ''')%dict(total=self.total, da=self.daily_average, ac=self.average,
+ ''')%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),
+ 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):
-
+
TRENDS = '/tmp/donations_trend.png'
MONTH_TRENDS = '/tmp/donations_month_trend.png'
-
+ AVERAGES = '/tmp/donations_averages.png'
+
def __init__(self, apache=False, root='/', data_file='/tmp/donations.xml'):
self.apache = apache
self.document_root = root
self.data_file = data_file
self.read_records()
-
+
+ def calculate_daily_averages(self):
+ stats = self.get_slice(self.earliest, self.latest)
+ fig = plt.figure(2, (10, 4), 96)#, facecolor, edgecolor, frameon, FigureClass)
+ fig.clear()
+ ax = fig.add_subplot(111)
+ month_order, month_averages = stats.get_daily_averages()
+ x = [date(y, m, 1) for y, m in month_order[:-1]]
+ ax.plot(x, month_averages[:-1])
+ ax.set_xlabel('Month')
+ ax.set_ylabel('Daily average ($)')
+ ax.xaxis.set_major_locator(mdates.MonthLocator(interval=2))
+ ax.xaxis.set_major_formatter(mdates.DateFormatter('%m/%y'))
+ fig.savefig(self.AVERAGES)
+
+
def calculate_month_trend(self, days=31):
stats = self.get_slice(date.today()-timedelta(days=days-1), date.today())
fig = plt.figure(2, (10, 4), 96)#, facecolor, edgecolor, frameon, FigureClass)
@@ -214,14 +252,14 @@ Total: $%(total).2f
Daily average: $%(da).2f \u00b1 %(dd).2f
Average contribution: $%(ac).2f \u00b1 %(ad).2f
Donors per day: %(dpd).2f
- '''%dict(total=stats.total, da=stats.daily_average,
+ '''%dict(total=stats.total, da=stats.daily_average,
dd=stats.daily_deviation, ac=stats.average,
ad=stats.average_deviation,
dpd=len(stats.totals)/float(stats.period.days),
)
text = ax.annotate(text, (0.5, 0.65), textcoords='axes fraction')
fig.savefig(self.MONTH_TRENDS)
-
+
def calculate_trend(self):
def months(start, end):
pos = range_for_month(start.year, start.month)[0]
@@ -253,7 +291,7 @@ Donors per day: %(dpd).2f
fig.savefig(self.TRENDS)
#plt.show()
-
+
def read_records(self):
self.tree = etree.parse(self.data_file)
self.last_read_time = time.time()
@@ -269,21 +307,22 @@ Donors per day: %(dpd).2f
self.earliest, self.latest = min_date, max_date
self.calculate_trend()
self.calculate_month_trend()
-
+ self.calculate_daily_averages()
+
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],
start_date, 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):
if os.stat(self.data_file).st_mtime >= self.last_read_time:
self.read_records()
@@ -294,7 +333,7 @@ Donors per day: %(dpd).2f
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):
@@ -302,7 +341,7 @@ Donors per day: %(dpd).2f
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:
@@ -312,11 +351,11 @@ Donors per day: %(dpd).2f
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':
@@ -332,9 +371,9 @@ Donors per day: %(dpd).2f
range_stats = 'Invalid input:\n%s
'%err
else:
range_stats = self.get_slice(*range_stats).to_html(num_of_countries=10)
-
+
today = self.get_slice(date.today(), date.today())
-
+
return textwrap.dedent('''\
@@ -365,7 +404,7 @@ Donors per day: %(dpd).2f
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)) {
@@ -381,11 +420,11 @@ Donors per day: %(dpd).2f
}
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)) {
@@ -409,8 +448,8 @@ Donors per day: %(dpd).2f
return false;
}
return true;
- }
-
+ }
+
function rationalize_periods() {
var form = document.forms[0];
var disabled = !form.period_type[0].checked;
@@ -438,7 +477,7 @@ Donors per day: %(dpd).2f
Donations to date
%(todate)s
-
+
Donations in period
|