Added modified version of the JQuery sparkline library.
--- a/ckanext/ga_report/controller.py
+++ b/ckanext/ga_report/controller.py
@@ -13,6 +13,7 @@
log = logging.getLogger('ckanext.ga-report')
+DOWNLOADS_AVAILABLE_FROM = '2012-12'
def _get_month_name(strdate):
import calendar
@@ -21,13 +22,34 @@
return '%s %s' % (calendar.month_name[d.tm_mon], d.tm_year)
-def _month_details(cls):
- '''Returns a list of all the month names'''
+def _month_details(cls, stat_key=None):
+ '''
+ Returns a list of all the periods for which we have data, unfortunately
+ knows too much about the type of the cls being passed as GA_Url has a
+ more complex query
+
+ This may need extending if we add a period_name to the stats
+ '''
months = []
- vals = model.Session.query(cls.period_name).filter(cls.period_name!='All').distinct().all()
+ day = None
+
+ q = model.Session.query(cls.period_name,cls.period_complete_day)\
+ .filter(cls.period_name!='All').distinct(cls.period_name)
+ if stat_key:
+ q= q.filter(cls.stat_name==stat_key)
+
+ vals = q.order_by("period_name desc").all()
+
+ if vals and vals[0][1]:
+ day = int(vals[0][1])
+ ordinal = 'th' if 11 <= day <= 13 \
+ else {1:'st',2:'nd',3:'rd'}.get(day % 10, 'th')
+ day = "{day}{ordinal}".format(day=day, ordinal=ordinal)
+
for m in vals:
months.append( (m[0], _get_month_name(m[0])))
- return sorted(months, key=operator.itemgetter(0), reverse=True)
+
+ return months, day
class GaReport(BaseController):
@@ -35,7 +57,7 @@
def csv(self, month):
import csv
- q = model.Session.query(GA_Stat)
+ q = model.Session.query(GA_Stat).filter(GA_Stat.stat_name!='Downloads')
if month != 'all':
q = q.filter(GA_Stat.period_name==month)
entries = q.order_by('GA_Stat.period_name, GA_Stat.stat_name, GA_Stat.key').all()
@@ -52,11 +74,12 @@
entry.key.encode('utf-8'),
entry.value.encode('utf-8')])
+
def index(self):
# Get the month details by fetching distinct values and determining the
# month names from the values.
- c.months = _month_details(GA_Stat)
+ c.months, c.day = _month_details(GA_Stat)
# Work out which month to show, based on query params of the first item
c.month_desc = 'all months'
@@ -71,13 +94,13 @@
entries = q.order_by('ga_stat.key').all()
def clean_key(key, val):
- if key in ['Average time on site', 'Pages per visit', 'New visits', 'Bounces']:
+ if key in ['Average time on site', 'Pages per visit', 'New visits', 'Bounce rate (home page)']:
val = "%.2f" % round(float(val), 2)
if key == 'Average time on site':
mins, secs = divmod(float(val), 60)
hours, mins = divmod(mins, 60)
val = '%02d:%02d:%02d (%s seconds) ' % (hours, mins, secs, val)
- if key in ['New visits','Bounces']:
+ if key in ['New visits','Bounce rate (home page)']:
val = "%s%%" % val
if key in ['Total page views', 'Total visits']:
val = int(val)
@@ -97,7 +120,7 @@
if k in ['Total page views', 'Total visits']:
v = sum(v)
else:
- v = float(sum(v))/len(v)
+ v = float(sum(v))/float(len(v))
key, val = clean_key(k,v)
c.global_totals.append((key, val))
@@ -206,13 +229,14 @@
str('attachment; filename=datasets_%s_%s.csv' % (c.publisher_name, month,))
writer = csv.writer(response)
- writer.writerow(["Dataset Title", "Dataset Name", "Views", "Visits", "Period Name"])
-
- for package,view,visit in packages:
+ writer.writerow(["Dataset Title", "Dataset Name", "Views", "Visits", "Resource downloads", "Period Name"])
+
+ for package,view,visit,downloads in packages:
writer.writerow([package.title.encode('utf-8'),
package.name.encode('utf-8'),
view,
visit,
+ downloads,
month])
def publishers(self):
@@ -220,7 +244,7 @@
# Get the month details by fetching distinct values and determining the
# month names from the values.
- c.months = _month_details(GA_Url)
+ c.months, c.day = _month_details(GA_Url)
# Work out which month to show, based on query params of the first item
c.month = request.params.get('month', '')
@@ -232,11 +256,11 @@
return render('ga_report/publisher/index.html')
def _get_packages(self, publisher=None, count=-1):
- '''Returns the datasets in order of visits'''
- if count == -1:
- count = sys.maxint
-
+ '''Returns the datasets in order of views'''
+ have_download_data = True
month = c.month or 'All'
+ if month != 'All':
+ have_download_data = month >= DOWNLOADS_AVAILABLE_FROM
q = model.Session.query(GA_Url,model.Package)\
.filter(model.Package.name==GA_Url.package_id)\
@@ -244,11 +268,27 @@
if publisher:
q = q.filter(GA_Url.department_id==publisher.name)
q = q.filter(GA_Url.period_name==month)
- q = q.order_by('ga_url.visitors::int desc')
+ q = q.order_by('ga_url.pageviews::int desc')
top_packages = []
- for entry,package in q.limit(count):
+ if count == -1:
+ entries = q.all()
+ else:
+ entries = q.limit(count)
+
+ for entry,package in entries:
if package:
- top_packages.append((package, entry.pageviews, entry.visitors))
+ # Downloads ....
+ if have_download_data:
+ dls = model.Session.query(GA_Stat).\
+ filter(GA_Stat.stat_name=='Downloads').\
+ filter(GA_Stat.key==package.name)
+ if month != 'All': # Fetch everything unless the month is specific
+ dls = dls.filter(GA_Stat.period_name==month)
+
+ downloads = sum(int(d.value) for d in dls.all())
+ else:
+ downloads = 'No data'
+ top_packages.append((package, entry.pageviews, entry.visits, downloads))
else:
log.warning('Could not find package associated package')
@@ -278,7 +318,7 @@
# Get the month details by fetching distinct values and determining the
# month names from the values.
- c.months = _month_details(GA_Url)
+ c.months, c.day = _month_details(GA_Url)
# Work out which month to show, based on query params of the first item
c.month = request.params.get('month', '')
@@ -306,13 +346,13 @@
month = c.month or 'All'
connection = model.Session.connection()
q = """
- select department_id, sum(pageviews::int) views, sum(visitors::int) visits
+ select department_id, sum(pageviews::int) views, sum(visits::int) visits
from ga_url
where department_id <> ''
and package_id <> ''
and url like '/dataset/%%'
and period_name=%s
- group by department_id order by visits desc
+ group by department_id order by views desc
"""
if limit:
q = q + " limit %s;" % (limit)
@@ -329,7 +369,7 @@
def _get_publishers():
'''
Returns a list of all publishers. Each item is a tuple:
- (names, title)
+ (name, title)
'''
publishers = []
for pub in model.Session.query(model.Group).\
--- a/ckanext/ga_report/download_analytics.py
+++ b/ckanext/ga_report/download_analytics.py
@@ -13,6 +13,7 @@
FORMAT_MONTH = '%Y-%m'
MIN_VIEWS = 50
MIN_VISITS = 20
+MIN_DOWNLOADS = 10
class DownloadAnalytics(object):
'''Downloads and stores analytics info'''
@@ -98,7 +99,7 @@
self.get_full_period_name(period_name, period_complete_day),
start_date.strftime('%Y-%m-%d'),
end_date.strftime('%Y-%m-%d'))
-
+
if self.delete_first:
log.info('Deleting existing Analytics for this period "%s"',
period_name)
@@ -122,11 +123,15 @@
log.info('Storing publisher views (%i rows)', len(data.get('url')))
self.store(period_name, period_complete_day, data,)
+ # Make sure the All records are correct.
+ ga_model.post_update_url_stats()
+
log.info('Aggregating datasets by publisher')
ga_model.update_publisher_stats(period_name) # about 30 seconds.
+
log.info('Downloading and storing analytics for site-wide stats')
- self.sitewide_stats( period_name )
+ self.sitewide_stats( period_name, period_complete_day )
log.info('Downloading and storing analytics for social networks')
self.update_social_info(period_name, start_date, end_date)
@@ -153,7 +158,8 @@
data = collections.defaultdict(list)
rows = results.get('rows',[])
for row in rows:
- data[_normalize_url(row[0])].append( (row[1], int(row[2]),) )
+ url = _normalize_url('http:/' + row[0])
+ data[url].append( (row[1], int(row[2]),) )
ga_model.update_social(period_name, data)
@@ -162,8 +168,8 @@
start_date = start_date.strftime('%Y-%m-%d')
end_date = end_date.strftime('%Y-%m-%d')
query = 'ga:pagePath=%s$' % path
- metrics = 'ga:uniquePageviews, ga:visits'
- sort = '-ga:uniquePageviews'
+ metrics = 'ga:pageviews, ga:visits'
+ sort = '-ga:pageviews'
# Supported query params at
# https://developers.google.com/analytics/devguides/reporting/core/v3/reference
@@ -178,6 +184,7 @@
end_date=end_date).execute()
packages = []
+ log.info("There are %d results" % results['totalResults'])
for entry in results.get('rows'):
(loc,pageviews,visits) = entry
url = _normalize_url('http:/' + loc) # strips off domain e.g. www.data.gov.uk or data.gov.uk
@@ -194,7 +201,7 @@
if 'url' in data:
ga_model.update_url_stats(period_name, period_complete_day, data['url'])
- def sitewide_stats(self, period_name):
+ def sitewide_stats(self, period_name, period_complete_day):
import calendar
year, month = period_name.split('-')
_, last_day_of_month = calendar.monthrange(int(year), int(month))
@@ -202,10 +209,10 @@
start_date = '%s-01' % period_name
end_date = '%s-%s' % (period_name, last_day_of_month)
funcs = ['_totals_stats', '_social_stats', '_os_stats',
- '_locale_stats', '_browser_stats', '_mobile_stats']
+ '_locale_stats', '_browser_stats', '_mobile_stats', '_download_stats']
for f in funcs:
log.info('Downloading analytics for %s' % f.split('_')[1])
- getattr(self, f)(start_date, end_date, period_name)
+ getattr(self, f)(start_date, end_date, period_name, period_complete_day)
def _get_results(result_data, f):
data = {}
@@ -214,17 +221,18 @@
data[key] = data.get(key,0) + result[1]
return data
- def _totals_stats(self, start_date, end_date, period_name):
+ def _totals_stats(self, start_date, end_date, period_name, period_complete_day):
""" Fetches distinct totals, total pageviews etc """
results = self.service.data().ga().get(
ids='ga:' + self.profile_id,
start_date=start_date,
- metrics='ga:uniquePageviews',
- sort='-ga:uniquePageviews',
- max_results=10000,
- end_date=end_date).execute()
- result_data = results.get('rows')
- ga_model.update_sitewide_stats(period_name, "Totals", {'Total page views': result_data[0][0]})
+ metrics='ga:pageviews',
+ sort='-ga:pageviews',
+ max_results=10000,
+ end_date=end_date).execute()
+ result_data = results.get('rows')
+ ga_model.update_sitewide_stats(period_name, "Totals", {'Total page views': result_data[0][0]},
+ period_complete_day)
results = self.service.data().ga().get(
ids='ga:' + self.profile_id,
@@ -239,7 +247,7 @@
'New visits': result_data[0][2],
'Total visits': result_data[0][3],
}
- ga_model.update_sitewide_stats(period_name, "Totals", data)
+ ga_model.update_sitewide_stats(period_name, "Totals", data, period_complete_day)
# Bounces from / or another configurable page.
path = '/%s%s' % (config.get('googleanalytics.account'),
@@ -248,29 +256,30 @@
ids='ga:' + self.profile_id,
filters='ga:pagePath==%s' % (path,),
start_date=start_date,
- metrics='ga:bounces,ga:uniquePageviews',
+ metrics='ga:visitBounceRate',
dimensions='ga:pagePath',
max_results=10000,
end_date=end_date).execute()
result_data = results.get('rows')
- if len(result_data) != 1:
+ if not result_data or len(result_data) != 1:
log.error('Could not pinpoint the bounces for path: %s. Got results: %r',
path, result_data)
return
results = result_data[0]
- bounces, total = [float(x) for x in result_data[0][1:]]
- pct = 100 * bounces/total
- log.info('%d bounces from %d total == %s', bounces, total, pct)
- ga_model.update_sitewide_stats(period_name, "Totals", {'Bounce rate': pct})
-
-
- def _locale_stats(self, start_date, end_date, period_name):
+ bounces = float(results[1])
+ # visitBounceRate is already a %
+ log.info('Google reports visitBounceRate as %s', bounces)
+ ga_model.update_sitewide_stats(period_name, "Totals", {'Bounce rate (home page)': float(bounces)},
+ period_complete_day)
+
+
+ def _locale_stats(self, start_date, end_date, period_name, period_complete_day):
""" Fetches stats about language and country """
results = self.service.data().ga().get(
ids='ga:' + self.profile_id,
start_date=start_date,
- metrics='ga:uniquePageviews',
- sort='-ga:uniquePageviews',
+ metrics='ga:pageviews',
+ sort='-ga:pageviews',
dimensions="ga:language,ga:country",
max_results=10000,
end_date=end_date).execute()
@@ -279,22 +288,78 @@
for result in result_data:
data[result[0]] = data.get(result[0], 0) + int(result[2])
self._filter_out_long_tail(data, MIN_VIEWS)
- ga_model.update_sitewide_stats(period_name, "Languages", data)
+ ga_model.update_sitewide_stats(period_name, "Languages", data, period_complete_day)
data = {}
for result in result_data:
data[result[1]] = data.get(result[1], 0) + int(result[2])
self._filter_out_long_tail(data, MIN_VIEWS)
- ga_model.update_sitewide_stats(period_name, "Country", data)
-
-
- def _social_stats(self, start_date, end_date, period_name):
+ ga_model.update_sitewide_stats(period_name, "Country", data, period_complete_day)
+
+
+ def _download_stats(self, start_date, end_date, period_name, period_complete_day):
+ """ Fetches stats about language and country """
+ import ckan.model as model
+
+ data = {}
+
+ results = self.service.data().ga().get(
+ ids='ga:' + self.profile_id,
+ start_date=start_date,
+ filters='ga:eventAction==download',
+ metrics='ga:totalEvents',
+ sort='-ga:totalEvents',
+ dimensions="ga:eventLabel",
+ max_results=10000,
+ end_date=end_date).execute()
+ result_data = results.get('rows')
+ if not result_data:
+ # We may not have data for this time period, so we need to bail
+ # early.
+ log.info("There is no download data for this time period")
+ return
+
+ def process_result_data(result_data, cached=False):
+ for result in result_data:
+ url = result[0].strip()
+
+ # Get package id associated with the resource that has this URL.
+ q = model.Session.query(model.Resource)
+ if cached:
+ r = q.filter(model.Resource.cache_url.like("%s%%" % url)).first()
+ else:
+ r = q.filter(model.Resource.url.like("%s%%" % url)).first()
+
+ package_name = r.resource_group.package.name if r else ""
+ if package_name:
+ data[package_name] = data.get(package_name, 0) + int(result[1])
+ else:
+ log.warning(u"Could not find resource for URL: {url}".format(url=url))
+ continue
+
+ process_result_data(results.get('rows'))
+
+ results = self.service.data().ga().get(
+ ids='ga:' + self.profile_id,
+ start_date=start_date,
+ filters='ga:eventAction==download-cache',
+ metrics='ga:totalEvents',
+ sort='-ga:totalEvents',
+ dimensions="ga:eventLabel",
+ max_results=10000,
+ end_date=end_date).execute()
+ process_result_data(results.get('rows'), cached=False)
+
+ self._filter_out_long_tail(data, MIN_DOWNLOADS)
+ ga_model.update_sitewide_stats(period_name, "Downloads", data, period_complete_day)
+
+ def _social_stats(self, start_date, end_date, period_name, period_complete_day):
""" Finds out which social sites people are referred from """
results = self.service.data().ga().get(
ids='ga:' + self.profile_id,
start_date=start_date,
- metrics='ga:uniquePageviews',
- sort='-ga:uniquePageviews',
+ metrics='ga:pageviews',
+ sort='-ga:pageviews',
dimensions="ga:socialNetwork,ga:referralPath",
max_results=10000,
end_date=end_date).execute()
@@ -304,16 +369,16 @@
if not result[0] == '(not set)':
data[result[0]] = data.get(result[0], 0) + int(result[2])
self._filter_out_long_tail(data, 3)
- ga_model.update_sitewide_stats(period_name, "Social sources", data)
-
-
- def _os_stats(self, start_date, end_date, period_name):
+ ga_model.update_sitewide_stats(period_name, "Social sources", data, period_complete_day)
+
+
+ def _os_stats(self, start_date, end_date, period_name, period_complete_day):
""" Operating system stats """
results = self.service.data().ga().get(
ids='ga:' + self.profile_id,
start_date=start_date,
- metrics='ga:uniquePageviews',
- sort='-ga:uniquePageviews',
+ metrics='ga:pageviews',
+ sort='-ga:pageviews',
dimensions="ga:operatingSystem,ga:operatingSystemVersion",
max_results=10000,
end_date=end_date).execute()
@@ -322,23 +387,23 @@
for result in result_data:
data[result[0]] = data.get(result[0], 0) + int(result[2])
self._filter_out_long_tail(data, MIN_VIEWS)
- ga_model.update_sitewide_stats(period_name, "Operating Systems", data)
+ ga_model.update_sitewide_stats(period_name, "Operating Systems", data, period_complete_day)
data = {}
for result in result_data:
if int(result[2]) >= MIN_VIEWS:
key = "%s %s" % (result[0],result[1])
data[key] = result[2]
- ga_model.update_sitewide_stats(period_name, "Operating Systems versions", data)
-
-
- def _browser_stats(self, start_date, end_date, period_name):
+ ga_model.update_sitewide_stats(period_name, "Operating Systems versions", data, period_complete_day)
+
+
+ def _browser_stats(self, start_date, end_date, period_name, period_complete_day):
""" Information about browsers and browser versions """
results = self.service.data().ga().get(
ids='ga:' + self.profile_id,
start_date=start_date,
- metrics='ga:uniquePageviews',
- sort='-ga:uniquePageviews',
+ metrics='ga:pageviews',
+ sort='-ga:pageviews',
dimensions="ga:browser,ga:browserVersion",
max_results=10000,
end_date=end_date).execute()
@@ -349,14 +414,14 @@
for result in result_data:
data[result[0]] = data.get(result[0], 0) + int(result[2])
self._filter_out_long_tail(data, MIN_VIEWS)
- ga_model.update_sitewide_stats(period_name, "Browsers", data)
+ ga_model.update_sitewide_stats(period_name, "Browsers", data, period_complete_day)
data = {}
for result in result_data:
key = "%s %s" % (result[0], self._filter_browser_version(result[0], result[1]))
data[key] = data.get(key, 0) + int(result[2])
self._filter_out_long_tail(data, MIN_VIEWS)
- ga_model.update_sitewide_stats(period_name, "Browser versions", data)
+ ga_model.update_sitewide_stats(period_name, "Browser versions", data, period_complete_day)
@classmethod
def _filter_browser_version(cls, browser, version_str):
@@ -380,14 +445,14 @@
ver = ver[0] + ver[1] + 'X' * num_hidden_digits
return ver
- def _mobile_stats(self, start_date, end_date, period_name):
+ def _mobile_stats(self, start_date, end_date, period_name, period_complete_day):
""" Info about mobile devices """
results = self.service.data().ga().get(
ids='ga:' + self.profile_id,
start_date=start_date,
- metrics='ga:uniquePageviews',
- sort='-ga:uniquePageviews',
+ metrics='ga:pageviews',
+ sort='-ga:pageviews',
dimensions="ga:mobileDeviceBranding, ga:mobileDeviceInfo",
max_results=10000,
end_date=end_date).execute()
@@ -397,13 +462,13 @@
for result in result_data:
data[result[0]] = data.get(result[0], 0) + int(result[2])
self._filter_out_long_tail(data, MIN_VIEWS)
- ga_model.update_sitewide_stats(period_name, "Mobile brands", data)
+ ga_model.update_sitewide_stats(period_name, "Mobile brands", data, period_complete_day)
data = {}
for result in result_data:
data[result[1]] = data.get(result[1], 0) + int(result[2])
self._filter_out_long_tail(data, MIN_VIEWS)
- ga_model.update_sitewide_stats(period_name, "Mobile devices", data)
+ ga_model.update_sitewide_stats(period_name, "Mobile devices", data, period_complete_day)
@classmethod
def _filter_out_long_tail(cls, data, threshold=10):
--- a/ckanext/ga_report/ga_model.py
+++ b/ckanext/ga_report/ga_model.py
@@ -9,6 +9,8 @@
import ckan.model as model
from ckan.lib.base import *
+
+log = __import__('logging').getLogger(__name__)
def make_uuid():
return unicode(uuid.uuid4())
@@ -27,7 +29,7 @@
Column('period_name', types.UnicodeText),
Column('period_complete_day', types.Integer),
Column('pageviews', types.UnicodeText),
- Column('visitors', types.UnicodeText),
+ Column('visits', types.UnicodeText),
Column('url', types.UnicodeText),
Column('department_id', types.UnicodeText),
Column('package_id', types.UnicodeText),
@@ -45,6 +47,7 @@
Column('id', types.UnicodeText, primary_key=True,
default=make_uuid),
Column('period_name', types.UnicodeText),
+ Column('period_complete_day', types.UnicodeText),
Column('stat_name', types.UnicodeText),
Column('key', types.UnicodeText),
Column('value', types.UnicodeText), )
@@ -63,7 +66,7 @@
Column('period_name', types.UnicodeText),
Column('publisher_name', types.UnicodeText),
Column('views', types.UnicodeText),
- Column('visitors', types.UnicodeText),
+ Column('visits', types.UnicodeText),
Column('toplevel', types.Boolean, default=False),
Column('subpublishercount', types.Integer, default=0),
Column('parent', types.UnicodeText),
@@ -132,7 +135,7 @@
return None, publisher_match.groups()[0]
return None, None
-def update_sitewide_stats(period_name, stat_name, data):
+def update_sitewide_stats(period_name, stat_name, data, period_complete_day):
for k,v in data.iteritems():
item = model.Session.query(GA_Stat).\
filter(GA_Stat.period_name==period_name).\
@@ -142,11 +145,13 @@
item.period_name = period_name
item.key = k
item.value = v
+ item.period_complete_day = period_complete_day
model.Session.add(item)
else:
# create the row
values = {'id': make_uuid(),
'period_name': period_name,
+ 'period_complete_day': period_complete_day,
'key': k,
'value': v,
'stat_name': stat_name
@@ -155,30 +160,57 @@
model.Session.commit()
-def update_url_stat_totals(period_name):
-
+def pre_update_url_stats(period_name):
+ log.debug("Deleting '%s' records" % period_name)
+ model.Session.query(GA_Url).\
+ filter(GA_Url.period_name==period_name).delete()
+
+ count = model.Session.query(GA_Url).\
+ filter(GA_Url.period_name == 'All').count()
+ log.debug("Deleting %d 'All' records" % count)
+ count = model.Session.query(GA_Url).\
+ filter(GA_Url.period_name == 'All').delete()
+ log.debug("Deleted %d 'All' records" % count)
+
+ model.Session.flush()
+ model.Session.commit()
+ model.repo.commit_and_remove()
+
+def post_update_url_stats():
+
+ """ Check the distinct url field in ga_url and make sure
+ it has an All record. If not then create one.
+
+ After running this then every URL should have an All
+ record regardless of whether the URL has an entry for
+ the month being currently processed.
"""
- items = model.Session.query(GA_Url).\
- filter(GA_Url.period_name != "All").\
- filter(GA_Url.url==url).all()
+ query = """select url, pageviews::int, visits::int
+ from ga_url
+ where url not in (select url from ga_url where period_name ='All')"""
+ connection = model.Session.connection()
+ res = connection.execute(query)
+