Changes related to presentation, and new tabs.
Changes related to presentation, and new tabs.

- Changed nearly all values to percentages of the total
- Added non-version info for OS and browsers
- Made tabs into dropdowns for Browser/OS

file:a/README.rst -> file:b/README.rst
ckanext-ga-report ckanext-ga-report
================= =================
   
**Status:** Development **Status:** Development
   
**CKAN Version:** 1.7.1+ **CKAN Version:** 1.7.1+
   
   
Overview Overview
-------- --------
   
For creating detailed reports of CKAN analytics, including totals per group. For creating detailed reports of CKAN analytics, including totals per group.
   
Whereas ckanext-googleanalytics focusses on providing page view stats a recent period and for all time (aimed at end users), ckanext-ga-report is more interested in building regular periodic reports (more for site managers to monitor). Whereas ckanext-googleanalytics focusses on providing page view stats a recent period and for all time (aimed at end users), ckanext-ga-report is more interested in building regular periodic reports (more for site managers to monitor).
   
Contents of this extension: Contents of this extension:
   
* Use the CLI tool to download Google Analytics data for each time period into this extension's database tables * Use the CLI tool to download Google Analytics data for each time period into this extension's database tables
   
* Users can view the data as web page reports * Users can view the data as web page reports
   
   
Installation Installation
------------ ------------
   
1. Activate you CKAN python environment and install this extension's software:: 1. Activate you CKAN python environment and install this extension's software::
   
$ pyenv/bin/activate $ pyenv/bin/activate
$ pip install -e git+https://github.com/okfn/ckanext-ga-report.git#egg=ckanext-ga-report $ pip install -e git+https://github.com/datagovuk/ckanext-ga-report.git#egg=ckanext-ga-report
   
2. Ensure you development.ini (or similar) contains the info about your Google Analytics account and configuration:: 2. Ensure you development.ini (or similar) contains the info about your Google Analytics account and configuration::
   
googleanalytics.id = UA-1010101-1 googleanalytics.id = UA-1010101-1
googleanalytics.account = Account name (i.e. data.gov.uk, see top level item at https://www.google.com/analytics) googleanalytics.account = Account name (i.e. data.gov.uk, see top level item at https://www.google.com/analytics)
ga-report.period = monthly ga-report.period = monthly
   
Note that your credentials will be readable by system administrators on your server. Rather than use sensitive account details, it is suggested you give access to the GA account to a new Google account that you create just for this purpose. Note that your credentials will be readable by system administrators on your server. Rather than use sensitive account details, it is suggested you give access to the GA account to a new Google account that you create just for this purpose.
   
3. Set up this extension's database tables using a paster command. (Ensure your CKAN pyenv is still activated, run the command from ``src/ckanext-ga-report``, alter the ``--config`` option to point to your site config file):: 3. Set up this extension's database tables using a paster command. (Ensure your CKAN pyenv is still activated, run the command from ``src/ckanext-ga-report``, alter the ``--config`` option to point to your site config file)::
   
$ paster initdb --config=../ckan/development.ini $ paster initdb --config=../ckan/development.ini
   
4. Enable the extension in your CKAN config file by adding it to ``ckan.plugins``:: 4. Enable the extension in your CKAN config file by adding it to ``ckan.plugins``::
   
ckan.plugins = ga-report ckan.plugins = ga-report
   
   
Authorization Authorization
-------------- --------------
   
Before you can access the data, you need to set up the OAUTH details which you can do by following the `instructions <https://developers.google.com/analytics/resources/tutorials/hello-analytics-api>`_ the outcome of which will be a file called credentials.json which should look like credentials.json.template with the relevant fields completed. These steps are below for convenience: Before you can access the data, you need to set up the OAUTH details which you can do by following the `instructions <https://developers.google.com/analytics/resources/tutorials/hello-analytics-api>`_ the outcome of which will be a file called credentials.json which should look like credentials.json.template with the relevant fields completed. These steps are below for convenience:
   
1. Visit the `Google APIs Console <https://code.google.com/apis/console>`_ 1. Visit the `Google APIs Console <https://code.google.com/apis/console>`_
   
2. Sign-in and create a project or use an existing project. 2. Sign-in and create a project or use an existing project.
   
3. In the `Services pane <https://code.google.com/apis/console#:services>`_ , activate Analytics API for your project. If prompted, read and accept the terms of service. 3. In the `Services pane <https://code.google.com/apis/console#:services>`_ , activate Analytics API for your project. If prompted, read and accept the terms of service.
   
4. Go to the `API Access pane <https://code.google.com/apis/console/#:access>`_ 4. Go to the `API Access pane <https://code.google.com/apis/console/#:access>`_
   
5. Click Create an OAuth 2.0 client ID.... 5. Click Create an OAuth 2.0 client ID....
   
6. Fill out the Branding Information fields and click Next. 6. Fill out the Branding Information fields and click Next.
   
7. In Client ID Settings, set Application type to Installed application. 7. In Client ID Settings, set Application type to Installed application.
   
8. Click Create client ID 8. Click Create client ID
   
9. The details you need below are Client ID, Client secret, and Redirect URIs 9. The details you need below are Client ID, Client secret, and Redirect URIs
   
   
Once you have set up your credentials.json file you can generate an oauth token file by using the Once you have set up your credentials.json file you can generate an oauth token file by using the
following command, which will store your oauth token in a file called token.dat once you have finished following command, which will store your oauth token in a file called token.dat once you have finished
giving permission in the browser:: giving permission in the browser::
   
$ paster getauthtoken --config=../ckan/development.ini $ paster getauthtoken --config=../ckan/development.ini
   
   
Tutorial Tutorial
-------- --------
   
Download some GA data and store it in CKAN's db. (Ensure your CKAN pyenv is still activated, run the command from ``src/ckanext-ga-report``, alter the ``--config`` option to point to your site config file) and specifying the name of your auth file (token.dat by default) from the previous step:: Download some GA data and store it in CKAN's db. (Ensure your CKAN pyenv is still activated, run the command from ``src/ckanext-ga-report``, alter the ``--config`` option to point to your site config file) and specifying the name of your auth file (token.dat by default) from the previous step::
   
$ paster loadanalytics token.dat latest --config=../ckan/development.ini $ paster loadanalytics token.dat latest --config=../ckan/development.ini
   
The value after the token file is how much data you want to retrieve, this can be The value after the token file is how much data you want to retrieve, this can be
   
* **all** - data for all time (since 2010) * **all** - data for all time (since 2010)
   
* **latest** - (default) just the 'latest' data * **latest** - (default) just the 'latest' data
   
* **YYYY-MM-DD** - just data for all time periods going back to (and including) this date * **YYYY-MM-DD** - just data for all time periods going back to (and including) this date
   
   
   
Software Licence Software Licence
================ ================
   
This software is developed by Cabinet Office. It is Crown Copyright and opened up under the Open Government Licence (OGL) (which is compatible with Creative Commons Attibution License). This software is developed by Cabinet Office. It is Crown Copyright and opened up under the Open Government Licence (OGL) (which is compatible with Creative Commons Attibution License).
   
OGL terms: http://www.nationalarchives.gov.uk/doc/open-government-licence/ OGL terms: http://www.nationalarchives.gov.uk/doc/open-government-licence/
   
import logging import logging
import datetime import datetime
   
from ckan.lib.cli import CkanCommand from ckan.lib.cli import CkanCommand
# No other CKAN imports allowed until _load_config is run, # No other CKAN imports allowed until _load_config is run,
# or logging is disabled # or logging is disabled
   
   
class InitDB(CkanCommand): class InitDB(CkanCommand):
"""Initialise the extension's database tables """Initialise the extension's database tables
""" """
summary = __doc__.split('\n')[0] summary = __doc__.split('\n')[0]
usage = __doc__ usage = __doc__
max_args = 0 max_args = 0
min_args = 0 min_args = 0
   
def command(self): def command(self):
self._load_config() self._load_config()
   
import ckan.model as model import ckan.model as model
model.Session.remove() model.Session.remove()
model.Session.configure(bind=model.meta.engine) model.Session.configure(bind=model.meta.engine)
log = logging.getLogger('ckanext.ga-report') log = logging.getLogger('ckanext.ga-report')
   
import ga_model import ga_model
ga_model.init_tables() ga_model.init_tables()
log.info("DB tables are setup") log.info("DB tables are setup")
   
   
class GetAuthToken(CkanCommand): class GetAuthToken(CkanCommand):
""" Get's the Google auth token """ Get's the Google auth token
   
Usage: paster getauthtoken <credentials_file> Usage: paster getauthtoken <credentials_file>
   
Where <credentials_file> is the file name containing the details Where <credentials_file> is the file name containing the details
for the service (obtained from https://code.google.com/apis/console). for the service (obtained from https://code.google.com/apis/console).
By default this is set to credentials.json By default this is set to credentials.json
""" """
summary = __doc__.split('\n')[0] summary = __doc__.split('\n')[0]
usage = __doc__ usage = __doc__
max_args = 0 max_args = 0
min_args = 0 min_args = 0
   
def command(self): def command(self):
""" """
In this case we don't want a valid service, but rather just to In this case we don't want a valid service, but rather just to
force the user through the auth flow. We allow this to complete to force the user through the auth flow. We allow this to complete to
act as a form of verification instead of just getting the token and act as a form of verification instead of just getting the token and
assuming it is correct. assuming it is correct.
""" """
from ga_auth import init_service from ga_auth import init_service
init_service('token.dat', init_service('token.dat',
self.args[0] if self.args self.args[0] if self.args
else 'credentials.json') else 'credentials.json')
   
   
class LoadAnalytics(CkanCommand): class LoadAnalytics(CkanCommand):
"""Get data from Google Analytics API and save it """Get data from Google Analytics API and save it
in the ga_model in the ga_model
   
Usage: paster loadanalytics <tokenfile> <time-period> Usage: paster loadanalytics <tokenfile> <time-period>
   
Where <tokenfile> is the name of the auth token file from Where <tokenfile> is the name of the auth token file from
the getauthtoken step. the getauthtoken step.
   
And where <time-period> is: And where <time-period> is:
all - data for all time all - data for all time
latest - (default) just the 'latest' data latest - (default) just the 'latest' data
YYYY-MM-DD - just data for all time periods going YYYY-MM - just data for the specific month
back to (and including) this date  
""" """
summary = __doc__.split('\n')[0] summary = __doc__.split('\n')[0]
usage = __doc__ usage = __doc__
max_args = 2 max_args = 2
min_args = 1 min_args = 1
   
def command(self): def command(self):
self._load_config() self._load_config()
   
from download_analytics import DownloadAnalytics from download_analytics import DownloadAnalytics
from ga_auth import (init_service, get_profile_id) from ga_auth import (init_service, get_profile_id)
   
try: try:
svc = init_service(self.args[0], None) svc = init_service(self.args[0], None)
except TypeError: except TypeError:
print ('Have you correctly run the getauthtoken task and ' print ('Have you correctly run the getauthtoken task and '
'specified the correct file here') 'specified the correct file here')
return return
   
downloader = DownloadAnalytics(svc, profile_id=get_profile_id(svc)) downloader = DownloadAnalytics(svc, profile_id=get_profile_id(svc))
   
time_period = self.args[1] if self.args and len(self.args) > 1 \ time_period = self.args[1] if self.args and len(self.args) > 1 \
else 'latest' else 'latest'
if time_period == 'all': if time_period == 'all':
downloader.all_() downloader.all_()
elif time_period == 'latest': elif time_period == 'latest':
downloader.latest() downloader.latest()
else: else:
since_date = datetime.datetime.strptime(time_period, '%Y-%m-%d') # The month to use
downloader.since_date(since_date) for_date = datetime.datetime.strptime(time_period, '%Y-%m')
  downloader.specific_month(for_date)
   
  import re
  import csv
  import sys
import logging import logging
from ckan.lib.base import BaseController, c, render import operator
import report_model import collections
  from ckan.lib.base import BaseController, c, render, request, response, abort
   
  import sqlalchemy
  from sqlalchemy import func, cast, Integer
  import ckan.model as model
  from ga_model import GA_Url, GA_Stat
   
log = logging.getLogger('ckanext.ga-report') log = logging.getLogger('ckanext.ga-report')
   
   
  def _get_month_name(strdate):
  import calendar
  from time import strptime
  d = strptime(strdate, '%Y-%m')
  return '%s %s' % (calendar.month_name[d.tm_mon], d.tm_year)
   
   
  def _month_details(cls):
  months = []
  vals = model.Session.query(cls.period_name).distinct().all()
  for m in vals:
  months.append( (m[0], _get_month_name(m[0])))
  return sorted(months, key=operator.itemgetter(0), reverse=True)
   
   
class GaReport(BaseController): class GaReport(BaseController):
   
  def csv(self, month):
  import csv
   
  q = model.Session.query(GA_Stat)
  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()
   
  response.headers['Content-Type'] = "text/csv; charset=utf-8"
  response.headers['Content-Disposition'] = str('attachment; filename=stats_%s.csv' % (month,))
   
  writer = csv.writer(response)
  writer.writerow(["Period", "Statistic", "Key", "Value"])
   
  for entry in entries:
  writer.writerow([entry.period_name.encode('utf-8'),
  entry.stat_name.encode('utf-8'),
  entry.key.encode('utf-8'),
  entry.value.encode('utf-8')])
   
def index(self): def index(self):
return render('index.html')  
  # Get the month details by fetching distinct values and determining the
  # month names from the values.
  c.months = _month_details(GA_Stat)
   
  # Work out which month to show, based on query params of the first item
  c.month_desc = 'all time'
  c.month = request.params.get('month', '')
  if c.month:
  c.month_desc = ''.join([m[1] for m in c.months if m[0]==c.month])
   
  q = model.Session.query(GA_Stat).\
  filter(GA_Stat.stat_name=='Totals')
  if c.month:
  q = q.filter(GA_Stat.period_name==c.month)
  entries = q.order_by('ga_stat.key').all()
   
  def clean_key(key, val):
  if key in ['Average time on site', 'Pages per visit', 'Percent new visits']:
  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 == 'Percent new visits':
  key = 'New visits'
  val = "%s%%" % val
  if key in ['Bounces', 'Total pageviews']:
  val = int(val)
  if key == 'Total pageviews':
  key = 'Total page views'
   
  return key, val
   
  c.global_totals = []
  if c.month:
  for e in entries:
  key, val = clean_key(e.key, e.value)
  c.global_totals.append((key, val))
  else:
  d = collections.defaultdict(list)
  for e in entries:
  d[e.key].append(float(e.value))
  for k, v in d.iteritems():
  if k in ['Bounces', 'Total pageviews']:
  v = sum(v)
  else:
  v = float(sum(v))/len(v)
  key, val = clean_key(k,v)
  c.global_totals.append((key, val))
  c.global_totals = sorted(c.global_totals, key=operator.itemgetter(0))
   
  keys = {
  'Browser versions': 'browser_versions',
  'Browsers': 'browsers',
  'Operating Systems versions': 'os_versions',
  'Operating Systems': 'os',
  'Social sources': 'social_networks',
  'Languages': 'languages',
  'Country': 'country'
  }
   
  browser_version_re = re.compile("(.*)\((.*)\)")
  for k, v in keys.iteritems():
   
  def clean_field(key):
  if k != 'Browser versions':
  return key
  m = browser_version_re.match(key)
  browser = m.groups()[0].strip()
  ver = m.groups()[1]
  parts = ver.split('.')
  if len(parts) > 1:
  if parts[1][0] == '0':
  ver = parts[0]
  else:
  ver = "%s.%s" % (parts[0],parts[1])
  if browser in ['Safari','Android Browser']: # Special case complex version nums
  ver = parts[0]
  if len(ver) > 2:
  ver = "%s%sX" % (ver[0], ver[1])
   
  return "%s (%s)" % (browser, ver,)
   
  q = model.Session.query(GA_Stat).\
  filter(GA_Stat.stat_name==k)
  if c.month:
  entries = []
  q = q.filter(GA_Stat.period_name==c.month).\
  order_by('ga_stat.value::int desc')
   
  d = collections.defaultdict(int)
  for e in q.all():
  d[clean_field(e.key)] += int(e.value)
  entries = []
  for key, val in d.iteritems():
  entries.append((key,val,))
  entries = sorted(entries, key=operator.itemgetter(1), reverse=True)
   
  def percent(num, total):
  p = 100 * float(num)/float(total)
  return "%.2f%%" % round(p, 2)
   
  # Get the total for each set of values and then set the value as
  # a percentage of the total
  total = sum([num for _,num in entries])
  setattr(c, v, [(k,percent(v,total)) for k,v in entries ])
   
  return render('ga_report/site/index.html')
   
   
  class GaPublisherReport(BaseController):
  """
  Displays the pageview and visit count for specific publishers based on
  the datasets associated with the publisher.
  """
  def csv(self, month):
   
  c.month = month if not month =='all' else ''
  response.headers['Content-Type'] = "text/csv; charset=utf-8"
  response.headers['Content-Disposition'] = str('attachment; filename=publishers_%s.csv' % (month,))
   
  writer = csv.writer(response)
  writer.writerow(["Publisher", "Views", "Visits", "Period Name"])
   
  for publisher,view,visit in self._get_publishers(None):
  writer.writerow([publisher.title.encode('utf-8'),
  view,
  visit,
  month])
   
   
   
  def publisher_csv(self, id, month):
   
  c.month = month if not month =='all' else ''
  c.publisher = model.Group.get(id)
  if not c.publisher:
  abort(404, 'A publisher with that name could not be found')
   
  packages = self._get_packages(c.publisher)
  response.headers['Content-Type'] = "text/csv; charset=utf-8"
  response.headers['Content-Disposition'] = \
  str('attachment; filename=%s_%s.csv' % (c.publisher.name, month,))
   
  writer = csv.writer(response)
  writer.writerow(["Publisher", "Views", "Visits", "Period Name"])
   
  for package,view,visit in packages:
  writer.writerow([package.title.encode('utf-8'),
  view,
  visit,
  month])
   
   
   
  def index(self):
   
  # Get the month details by fetching distinct values and determining the
  # month names from the values.
  c.months = _month_details(GA_Url)
   
  # Work out which month to show, based on query params of the first item
  c.month = request.params.get('month', '')
  c.month_desc = 'all time'
  if c.month:
  c.month_desc = ''.join([m[1] for m in c.months if m[0]==c.month])
   
  c.top_publishers = self._get_publishers()
   
  return render('ga_report/publisher/index.html')
   
  def _get_publishers(self, limit=20):
  connection = model.Session.connection()
  q = """
  select department_id, sum(pageviews::int) views, sum(visitors::int) visits
  from ga_url
  where department_id <> ''"""
  if c.month:
  q = q + """
  and period_name=%s
  """
  q = q + """
  group by department_id order by views desc
  """
  if limit:
  q = q + " limit %s;" % (limit)
   
  # Add this back (before and period_name =%s) if you want to ignore publisher
  # homepage views
  # and not url like '/publisher/%%'
   
  top_publishers = []
  res = connection.execute(q, c.month)
   
  for row in res:
  g = model.Group.get(row[0])
  if g:
  top_publishers.append((g, row[1], row[2]))
  return top_publishers
   
  def _get_packages(self, publisher, count=-1):
  if count == -1:
  count = sys.maxint
   
  top_packages = []
  q = model.Session.query(GA_Url).\
  filter(GA_Url.department_id==publisher.name).\
  filter(GA_Url.url.like('/dataset/%'))
  if c.month:
  q = q.filter(GA_Url.period_name==c.month)
  q = q.order_by('ga_url.pageviews::int desc')
   
  if c.month:
  for entry in q[:count]:
  p = model.Package.get(entry.url[len('/dataset/'):])
  top_packages.append((p,entry.pageviews,entry.visitors))
  else:
  ds = {}
  for entry in q.all():
  if len(ds) >= count:
  break
  p = model.Package.get(entry.url[len('/dataset/'):])
  if not p in ds:
  ds[p] = {'views':0, 'visits': 0}
  ds[p]['views'] = ds[p]['views'] + int(entry.pageviews)
  ds[p]['visits'] = ds[p]['visits'] + int(entry.visitors)
   
  results = []
  for k, v in ds.iteritems():
  results.append((k,v['views'],v['visits']))
   
  top_packages = sorted(results, key=operator.itemgetter(1), reverse=True)
  return top_packages
   
   
  def read(self, id):
  count = 20
   
  c.publisher = model.Group.get(id)
  if not c.publisher:
  abort(404, 'A publisher with that name could not be found')
  c.top_packages = [] # package, dataset_views in c.top_packages
   
  # Get the month details by fetching distinct values and determining the
  # month names from the values.
  c.months = _month_details(GA_Url)
   
  # Work out which month to show, based on query params of the first item
  c.month = request.params.get('month', '')
  if not c.month:
  c.month_desc = 'all time'
  else:
  c.month_desc = ''.join([m[1] for m in c.months if m[0]==c.month])
   
  c.publisher_page_views = 0
  q = model.Session.query(GA_Url).\
  filter(GA_Url.url=='/publisher/%s' % c.publisher.name)
  if c.month:
  entry = q.filter(GA_Url.period_name==c.month).first()
  c.publisher_page_views = entry.pageviews if entry else 0
  else:
  for e in q.all():
  c.publisher_page_views = c.publisher_page_views + int(e.pageviews)
   
  c.top_packages = self._get_packages(c.publisher, 20)
   
  return render('ga_report/publisher/read.html')
   
import os import os
import logging import logging
import datetime import datetime
   
from pylons import config from pylons import config
   
import ga_model import ga_model
   
#from ga_client import GA #from ga_client import GA
   
log = logging.getLogger('ckanext.ga-report') log = logging.getLogger('ckanext.ga-report')
   
FORMAT_MONTH = '%Y-%m' FORMAT_MONTH = '%Y-%m'
   
class DownloadAnalytics(object): class DownloadAnalytics(object):
'''Downloads and stores analytics info''' '''Downloads and stores analytics info'''
   
def __init__(self, service=None, profile_id=None): def __init__(self, service=None, profile_id=None):
self.period = config['ga-report.period'] self.period = config['ga-report.period']
self.service = service self.service = service
self.profile_id = profile_id self.profile_id = profile_id
   
   
def all_(self): def specific_month(self, date):
self.since_date(datetime.datetime(2010, 1, 1)) import calendar
   
  first_of_this_month = datetime.datetime(date.year, date.month, 1)
  _, last_day_of_month = calendar.monthrange(int(date.year), int(date.month))
  last_of_this_month = datetime.datetime(date.year, date.month, last_day_of_month)
  periods = ((date.strftime(FORMAT_MONTH),
  last_day_of_month,
  first_of_this_month, last_of_this_month),)
  self.download_and_store(periods)
   
   
def latest(self): def latest(self):
if self.period == 'monthly': if self.period == 'monthly':
# from first of this month to today # from first of this month to today
now = datetime.datetime.now() now = datetime.datetime.now()
first_of_this_month = datetime.datetime(now.year, now.month, 1) first_of_this_month = datetime.datetime(now.year, now.month, 1)
periods = ((now.strftime(FORMAT_MONTH), periods = ((now.strftime(FORMAT_MONTH),
now.day, now.day,
first_of_this_month, now),) first_of_this_month, now),)
else: else:
raise NotImplementedError raise NotImplementedError
self.download_and_store(periods) self.download_and_store(periods)
   
   
def since_date(self, since_date): def for_date(self, for_date):
assert isinstance(since_date, datetime.datetime) assert isinstance(since_date, datetime.datetime)
periods = [] # (period_name, period_complete_day, start_date, end_date) periods = [] # (period_name, period_complete_day, start_date, end_date)
if self.period == 'monthly': if self.period == 'monthly':
first_of_the_months_until_now = [] first_of_the_months_until_now = []
year = since_date.year year = for_date.year
month = since_date.month month = for_date.month
now = datetime.datetime.now() now = datetime.datetime.now()
first_of_this_month = datetime.datetime(now.year, now.month, 1) first_of_this_month = datetime.datetime(now.year, now.month, 1)
while True: while True:
first_of_the_month = datetime.datetime(year, month, 1) first_of_the_month = datetime.datetime(year, month, 1)
if first_of_the_month == first_of_this_month: if first_of_the_month == first_of_this_month:
periods.append((now.strftime(FORMAT_MONTH), periods.append((now.strftime(FORMAT_MONTH),
now.day, now.day,
first_of_this_month, now)) first_of_this_month, now))
break break
elif first_of_the_month < first_of_this_month: elif first_of_the_month < first_of_this_month:
in_the_next_month = first_of_the_month + datetime.timedelta(40) in_the_next_month = first_of_the_month + datetime.timedelta(40)
last_of_the_month = datetime.datetime(in_the_next_month.year, last_of_the_month = datetime.datetime(in_the_next_month.year,
in_the_next_month.month, 1)\ in_the_next_month.month, 1)\
- datetime.timedelta(1) - datetime.timedelta(1)
periods.append((now.strftime(FORMAT_MONTH), 0, periods.append((now.strftime(FORMAT_MONTH), 0,
first_of_the_month, last_of_the_month)) first_of_the_month, last_of_the_month))
else: else:
# first_of_the_month has got to the future somehow # first_of_the_month has got to the future somehow
break break
month += 1 month += 1
if month > 12: if month > 12:
year += 1 year += 1
month = 1 month = 1
else: else:
raise NotImplementedError raise NotImplementedError
self.download_and_store(periods) self.download_and_store(periods)
   
@staticmethod @staticmethod
def get_full_period_name(period_name, period_complete_day): def get_full_period_name(period_name, period_complete_day):
if period_complete_day: if period_complete_day:
return period_name + ' (up to %ith)' % period_complete_day return period_name + ' (up to %ith)' % period_complete_day
else: else:
return period_name return period_name
   
   
def download_and_store(self, periods): def download_and_store(self, periods):
for period_name, period_complete_day, start_date, end_date in periods: for period_name, period_complete_day, start_date, end_date in periods:
log.info('Downloading Analytics for period "%s" (%s - %s)', log.info('Downloading Analytics for period "%s" (%s - %s)',
self.get_full_period_name(period_name, period_complete_day), self.get_full_period_name(period_name, period_complete_day),
start_date.strftime('%Y %m %d'), start_date.strftime('%Y %m %d'),
end_date.strftime('%Y %m %d')) end_date.strftime('%Y %m %d'))
"""  
data = self.download(start_date, end_date, '~/dataset/[a-z0-9-_]+') data = self.download(start_date, end_date, '~/dataset/[a-z0-9-_]+')
log.info('Storing Dataset Analytics for period "%s"', log.info('Storing Dataset Analytics for period "%s"',
self.get_full_period_name(period_name, period_complete_day)) self.get_full_period_name(period_name, period_complete_day))
self.store(period_name, period_complete_day, data, ) self.store(period_name, period_complete_day, data, )
   
data = self.download(start_date, end_date, '~/publisher/[a-z0-9-_]+') data = self.download(start_date, end_date, '~/publisher/[a-z0-9-_]+')
log.info('Storing Publisher Analytics for period "%s"', log.info('Storing Publisher Analytics for period "%s"',
self.get_full_period_name(period_name, period_complete_day)) self.get_full_period_name(period_name, period_complete_day))
self.store(period_name, period_complete_day, data,) self.store(period_name, period_complete_day, data,)
"""  
ga_model.update_publisher_stats(period_name) # about 30 seconds. ga_model.update_publisher_stats(period_name) # about 30 seconds.
self.sitewide_stats( period_name ) self.sitewide_stats( period_name )
   
   
def download(self, start_date, end_date, path='~/dataset/[a-z0-9-_]+'): def download(self, start_date, end_date, path='~/dataset/[a-z0-9-_]+'):
'''Get data from GA for a given time period''' '''Get data from GA for a given time period'''
start_date = start_date.strftime('%Y-%m-%d') start_date = start_date.strftime('%Y-%m-%d')
end_date = end_date.strftime('%Y-%m-%d') end_date = end_date.strftime('%Y-%m-%d')
query = 'ga:pagePath=%s$' % path query = 'ga:pagePath=%s$' % path
metrics = 'ga:uniquePageviews, ga:visitors' metrics = 'ga:uniquePageviews, ga:visitors'
sort = '-ga:uniquePageviews' sort = '-ga:uniquePageviews'
   
# Supported query params at # Supported query params at
# https://developers.google.com/analytics/devguides/reporting/core/v3/reference # https://developers.google.com/analytics/devguides/reporting/core/v3/reference
results = self.service.data().ga().get( results = self.service.data().ga().get(
ids='ga:' + self.profile_id, ids='ga:' + self.profile_id,
filters=query, filters=query,
start_date=start_date, start_date=start_date,
metrics=metrics, metrics=metrics,
sort=sort, sort=sort,
dimensions="ga:pagePath", dimensions="ga:pagePath",
max_results=10000, max_results=10000,
end_date=end_date).execute() end_date=end_date).execute()
   
if os.getenv('DEBUG'): if os.getenv('DEBUG'):
import pprint import pprint
pprint.pprint(results) pprint.pprint(results)
print 'Total results: %s' % results.get('totalResults') print 'Total results: %s' % results.get('totalResults')
   
packages = [] packages = []
for entry in results.get('rows'): for entry in results.get('rows'):
(loc,pageviews,visits) = entry (loc,pageviews,visits) = entry
packages.append( ('http:/' + loc, pageviews, visits,) ) # Temporary hack packages.append( ('http:/' + loc, pageviews, visits,) ) # Temporary hack
return dict(url=packages) return dict(url=packages)
   
def store(self, period_name, period_complete_day, data): def store(self, period_name, period_complete_day, data):
if 'url' in data: if 'url' in data:
ga_model.update_url_stats(period_name, period_complete_day, data['url']) ga_model.update_url_stats(period_name, period_complete_day, data['url'])
   
def sitewide_stats(self, period_name): def sitewide_stats(self, period_name):
import calendar import calendar
year, month = period_name.split('-') year, month = period_name.split('-')
_, last_day_of_month = calendar.monthrange(int(year), int(month)) _, last_day_of_month = calendar.monthrange(int(year), int(month))
   
start_date = '%s-01' % period_name start_date = '%s-01' % period_name
end_date = '%s-%s' % (period_name, last_day_of_month) end_date = '%s-%s' % (period_name, last_day_of_month)
print 'Sitewide_stats for %s (%s -> %s)' % (period_name, start_date, end_date) print 'Sitewide_stats for %s (%s -> %s)' % (period_name, start_date, end_date)
   
funcs = ['_totals_stats', '_social_stats', '_os_stats', funcs = ['_totals_stats', '_social_stats', '_os_stats',
'_locale_stats', '_browser_stats', '_mobile_stats'] '_locale_stats', '_browser_stats', '_mobile_stats']
for f in funcs: for f in funcs:
print ' + Fetching %s stats' % f.split('_')[1] print ' + Fetching %s stats' % f.split('_')[1]
getattr(self, f)(start_date, end_date, period_name) getattr(self, f)(start_date, end_date, period_name)
   
def _get_results(result_data, f): def _get_results(result_data, f):
data = {} data = {}
for result in result_data: for result in result_data:
key = f(result) key = f(result)
data[key] = data.get(key,0) + result[1] data[key] = data.get(key,0) + result[1]
return data return data
   
def _totals_stats(self, start_date, end_date, period_name): def _totals_stats(self, start_date, end_date, period_name):
""" Fetches distinct totals, total pageviews etc """ """ Fetches distinct totals, total pageviews etc """
results = self.service.data().ga().get( results = self.service.data().ga().get(
ids='ga:' + self.profile_id, ids='ga:' + self.profile_id,
start_date=start_date, start_date=start_date,
metrics='ga:uniquePageviews', metrics='ga:uniquePageviews',
sort='-ga:uniquePageviews', sort='-ga:uniquePageviews',
max_results=10000, max_results=10000,
end_date=end_date).execute() end_date=end_date).execute()
result_data = results.get('rows') result_data = results.get('rows')
ga_model.update_sitewide_stats(period_name, "Totals", {'Total pageviews': result_data[0][0]}) ga_model.update_sitewide_stats(period_name, "Totals", {'Total pageviews': result_data[0][0]})
   
results = self.service.data().ga().get( results = self.service.data().ga().get(
ids='ga:' + self.profile_id, ids='ga:' + self.profile_id,
start_date=start_date, start_date=start_date,
metrics='ga:pageviewsPerVisit,ga:bounces,ga:avgTimeOnSite,ga:percentNewVisits', metrics='ga:pageviewsPerVisit,ga:bounces,ga:avgTimeOnSite,ga:percentNewVisits',
max_results=10000, max_results=10000,
end_date=end_date).execute() end_date=end_date).execute()
result_data = results.get('rows') result_data = results.get('rows')
data = { data = {
'Pages per visit': result_data[0][0], 'Pages per visit': result_data[0][0],
'Bounces': result_data[0][1], 'Bounces': result_data[0][1],
'Average time on site': result_data[0][2], 'Average time on site': result_data[0][2],
'Percent new visits': result_data[0][3], 'Percent new visits': result_data[0][3],
} }
ga_model.update_sitewide_stats(period_name, "Totals", data) ga_model.update_sitewide_stats(period_name, "Totals", data)
   
   
def _locale_stats(self, start_date, end_date, period_name): def _locale_stats(self, start_date, end_date, period_name):
""" Fetches stats about language and country """ """ Fetches stats about language and country """
results = self.service.data().ga().get( results = self.service.data().ga().get(
ids='ga:' + self.profile_id, ids='ga:' + self.profile_id,
start_date=start_date, start_date=start_date,
metrics='ga:uniquePageviews', metrics='ga:uniquePageviews',
sort='-ga:uniquePageviews', sort='-ga:uniquePageviews',
dimensions="ga:language,ga:country", dimensions="ga:language,ga:country",
max_results=10000, max_results=10000,
end_date=end_date).execute() end_date=end_date).execute()
result_data = results.get('rows') result_data = results.get('rows')
data = {} data = {}
for result in result_data: for result in result_data:
data[result[0]] = data.get(result[0], 0) + int(result[2]) data[result[0]] = data.get(result[0], 0) + int(result[2])
ga_model.update_sitewide_stats(period_name, "Languages", data) ga_model.update_sitewide_stats(period_name, "Languages", data)
   
data = {} data = {}
for result in result_data: for result in result_data:
data[result[1]] = data.get(result[1], 0) + int(result[2]) data[result[1]] = data.get(result[1], 0) + int(result[2])
ga_model.update_sitewide_stats(period_name, "Country", data) ga_model.update_sitewide_stats(period_name, "Country", data)
   
   
def _social_stats(self, start_date, end_date, period_name): def _social_stats(self, start_date, end_date, period_name):
""" Finds out which social sites people are referred from """ """ Finds out which social sites people are referred from """
results = self.service.data().ga().get( results = self.service.data().ga().get(
ids='ga:' + self.profile_id, ids='ga:' + self.profile_id,
start_date=start_date, start_date=start_date,
metrics='ga:uniquePageviews', metrics='ga:uniquePageviews',
sort='-ga:uniquePageviews', sort='-ga:uniquePageviews',
dimensions="ga:socialNetwork,ga:referralPath", dimensions="ga:socialNetwork,ga:referralPath",
max_results=10000, max_results=10000,
end_date=end_date).execute() end_date=end_date).execute()
result_data = results.get('rows') result_data = results.get('rows')
twitter_links = [] twitter_links = []
data = {} data = {}
for result in result_data: for result in result_data:
if not result[0] == '(not set)': if not result[0] == '(not set)':
data[result[0]] = data.get(result[0], 0) + int(result[2]) data[result[0]] = data.get(result[0], 0) + int(result[2])
if result[0] == 'Twitter': if result[0] == 'Twitter':
twitter_links.append(result[1]) twitter_links.append(result[1])
ga_model.update_sitewide_stats(period_name, "Social sources", data) ga_model.update_sitewide_stats(period_name, "Social sources", data)
   
   
def _os_stats(self, start_date, end_date, period_name): def _os_stats(self, start_date, end_date, period_name):
""" Operating system stats """ """ Operating system stats """
results = self.service.data().ga().get( results = self.service.data().ga().get(
ids='ga:' + self.profile_id, ids='ga:' + self.profile_id,
start_date=start_date, start_date=start_date,
metrics='ga:uniquePageviews', metrics='ga:uniquePageviews',
sort='-ga:uniquePageviews', sort='-ga:uniquePageviews',
dimensions="ga:operatingSystem,ga:operatingSystemVersion", dimensions="ga:operatingSystem,ga:operatingSystemVersion",
max_results=10000, max_results=10000,
end_date=end_date).execute() end_date=end_date).execute()
result_data = results.get('rows') result_data = results.get('rows')
data = {} data = {}
for result in result_data: for result in result_data:
data[result[0]] = data.get(result[0], 0) + int(result[2]) data[result[0]] = data.get(result[0], 0) + int(result[2])
ga_model.update_sitewide_stats(period_name, "Operating Systems", data) ga_model.update_sitewide_stats(period_name, "Operating Systems", data)
   
data = {} data = {}
for result in result_data: for result in result_data:
key = "%s (%s)" % (result[0],result[1]) key = "%s (%s)" % (result[0],result[1])
data[key] = result[2] data[key] = result[2]
ga_model.update_sitewide_stats(period_name, "Operating Systems versions", data) ga_model.update_sitewide_stats(period_name, "Operating Systems versions", data)
   
   
def _browser_stats(self, start_date, end_date, period_name): def _browser_stats(self, start_date, end_date, period_name):
""" Information about browsers and browser versions """ """ Information about browsers and browser versions """
results = self.service.data().ga().get( results = self.service.data().ga().get(
ids='ga:' + self.profile_id, ids='ga:' + self.profile_id,
start_date=start_date, start_date=start_date,
metrics='ga:uniquePageviews', metrics='ga:uniquePageviews',
sort='-ga:uniquePageviews', sort='-ga:uniquePageviews',
dimensions="ga:browser,ga:browserVersion", dimensions="ga:browser,ga:browserVersion",
max_results=10000, max_results=10000,
end_date=end_date).execute() end_date=end_date).execute()
result_data = results.get('rows') result_data = results.get('rows')
data = {} data = {}
for result in result_data: for result in result_data:
data[result[0]] = data.get(result[0], 0) + int(result[2]) data[result[0]] = data.get(result[0], 0) + int(result[2])
ga_model.update_sitewide_stats(period_name, "Browsers", data) ga_model.update_sitewide_stats(period_name, "Browsers", data)
   
data = {} data = {}
for result in result_data: for result in result_data:
key = "%s (%s)" % (result[0], result[1]) key = "%s (%s)" % (result[0], result[1])
data[key] = result[2] data[key] = result[2]
ga_model.update_sitewide_stats(period_name, "Browser versions", data) ga_model.update_sitewide_stats(period_name, "Browser versions", data)
   
   
def _mobile_stats(self, start_date, end_date, period_name): def _mobile_stats(self, start_date, end_date, period_name):
""" Info about mobile devices """ """ Info about mobile devices """
   
results = self.service.data().ga().get( results = self.service.data().ga().get(
ids='ga:' + self.profile_id, ids='ga:' + self.profile_id,
start_date=start_date, start_date=start_date,
metrics='ga:uniquePageviews', metrics='ga:uniquePageviews',
sort='-ga:uniquePageviews', sort='-ga:uniquePageviews',
dimensions="ga:mobileDeviceBranding, ga:mobileDeviceInfo", dimensions="ga:mobileDeviceBranding, ga:mobileDeviceInfo",
max_results=10000, max_results=10000,
end_date=end_date).execute() end_date=end_date).execute()
   
result_data = results.get('rows') result_data = results.get('rows')
data = {} data = {}
for result in result_data: for result in result_data:
data[result[0]] = data.get(result[0], 0) + int(result[2]) data[result[0]] = data.get(result[0], 0) + int(result[2])
ga_model.update_sitewide_stats(period_name, "Mobile brands", data) ga_model.update_sitewide_stats(period_name, "Mobile brands", data)
   
data = {} data = {}
for result in result_data: for result in result_data:
data[result[1]] = data.get(result[1], 0) + int(result[2]) data[result[1]] = data.get(result[1], 0) + int(result[2])
ga_model.update_sitewide_stats(period_name, "Mobile devices", data) ga_model.update_sitewide_stats(period_name, "Mobile devices", data)
   
  import logging
  import operator
  import ckan.lib.base as base
  import ckan.model as model
 
  _log = logging.getLogger(__name__)
 
  def most_popular_datasets(publisher, count=20):
  from ckanext.ga_report.ga_model import GA_Url
 
  if not publisher:
  _log.error("No valid publisher passed to 'most_popular_datasets'")
  return ""
 
  datasets = {}
  entries = model.Session.query(GA_Url).\
  filter(GA_Url.department_id==publisher.name).\
  filter(GA_Url.url.like('/dataset/%')).\
  order_by('ga_url.pageviews::int desc').all()
  for entry in entries:
  if len(datasets) < count:
  p = model.Package.get(entry.url[len('/dataset/'):])
  if not p in datasets:
  datasets[p] = {'views':0, 'visits': 0}
  datasets[p]['views'] = datasets[p]['views'] + int(entry.pageviews)
  datasets[p]['visits'] = datasets[p]['visits'] + int(entry.visitors)
 
  results = []
  for k, v in datasets.iteritems():
  results.append((k,v['views'],v['visits']))
 
  results = sorted(results, key=operator.itemgetter(1), reverse=True)
 
  ctx = {
  'dataset_count': len(datasets),
  'datasets': results,
 
  'publisher': publisher
  }
 
  return base.render_snippet('ga_report/publisher/popular.html', **ctx)
 
import logging import logging
import ckan.lib.helpers as h import ckan.lib.helpers as h
  import ckan.plugins as p
from ckan.plugins import implements, toolkit from ckan.plugins import implements, toolkit
import gasnippet  
import commands  
import dbutil  
   
log = logging.getLogger('ckanext.ga-report') log = logging.getLogger('ckanext.ga-report')
   
class GoogleAnalyticsPlugin(p.SingletonPlugin): class GAReportPlugin(p.SingletonPlugin):
implements(p.IConfigurer, inherit=True) implements(p.IConfigurer, inherit=True)
implements(p.IRoutes, inherit=True) implements(p.IRoutes, inherit=True)
  implements(p.ITemplateHelpers, inherit=True)
   
def update_config(self, config): def update_config(self, config):
toolkit.add_template_directory(config, 'templates') toolkit.add_template_directory(config, 'templates')
toolkit.add_public_directory(config, 'public') toolkit.add_public_directory(config, 'public')
   
  def get_helpers(self):
  """
  A dictionary of extra helpers that will be available to provide
  ga report info to templates.
  """
  from ckanext.ga_report.helpers import most_popular_datasets
  return {
  'ga_report_installed': lambda: True,
  'most_popular_datasets': most_popular_datasets,
  }
   
def after_map(self, map): def after_map(self, map):
map.connect( map.connect(
'/data/analytics/index', '/data/site-usage/publisher',
controller='ckanext.ga-report.controller:GaReport', controller='ckanext.ga_report.controller:GaPublisherReport',
action='index' action='index'
  )
  map.connect(
  '/data/site-usage/publisher_{month}.csv',
  controller='ckanext.ga_report.controller:GaPublisherReport',
  action='csv'
  )
  map.connect(
  '/data/site-usage/publisher/{id}_{month}.csv',
  controller='ckanext.ga_report.controller:GaPublisherReport',
  action='publisher_csv'
  )
  map.connect(
  '/data/site-usage/publisher/{id}',
  controller='ckanext.ga_report.controller:GaPublisherReport',
  action='read'
  )
  map.connect(
  '/data/site-usage',
  controller='ckanext.ga_report.controller:GaReport',
  action='index'
  )
  map.connect(
  '/data/site-usage/data_{month}.csv',
  controller='ckanext.ga_report.controller:GaReport',
  action='csv'
) )
return map return map
   
   
  <html
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:i18n="http://genshi.edgewall.org/i18n"
  xmlns:py="http://genshi.edgewall.org/"
  xmlns:xi="http://www.w3.org/2001/XInclude"
  py:strip=""
  >
 
  <table py:def="publisher_list(groups)" class="groups">
  <py:for each="group,title in groups">
  <tr>
  <td><a href="/publisher/${group.name}">${title}</a></td>
  </tr>
  </py:for>
  </table>
 
 
  <table py:def="stat_table(items)" class="table table-condensed table-bordered table-striped">
  <tr>
  <th>Name</th>
  <th>% Views</th>
  </tr>
  <py:for each="name, value in items">
  <tr>
  <td>${name}</td>
  <td>${value}</td>
  </tr>
  </py:for>
  </table>
 
 
  <div py:def="usage_nav(active_name,publisher)" id="minornavigation">
  <div id="minornavigation-bg-left">
  <div id="minornavigation-bg-right">
  <ul class="nav nav-pills">
  <li py:attrs="{'class': 'active' if active_name=='Site-wide' else None}"><a py:attrs="{'class': 'active' if active_name=='Site-wide' else None}" href="${h.url_for(controller='ckanext.ga_report.controller:GaReport',action='index')}"><img src="/images/icons/page_white_gear.png" height="16px" width="16px" alt="None" class="inline-icon "/> Site-wide</a></li>
  <li py:attrs="{'class': 'active' if active_name=='Publishers' else None}">
  <a py:attrs="{'class': 'active' if active_name=='Publishers' else None}" href="${h.url_for(controller='ckanext.ga_report.controller:GaPublisherReport',action='index')}"><img src="/images/icons/page_white_gear.png" height="16px" width="16px" alt="None" class="inline-icon "/> Publishers</a>
  </li>
  <li py:if="publisher" class="active">
  <a class="active" href="${h.url_for(controller='ckanext.ga_report.controller:GaPublisherReport',action='read', id=publisher.name)}"><img src="/images/icons/page_white_gear.png" height="16px" width="16px" alt="None" class="inline-icon "/>${publisher.title}</a>
  </li>
 
  </ul>
  </div>
  </div>
  </div>
 
 
  </html>
 
  <html xmlns:py="http://genshi.edgewall.org/"
  xmlns:i18n="http://genshi.edgewall.org/i18n"
  xmlns:xi="http://www.w3.org/2001/XInclude"
  py:strip="">
 
  <xi:include href="../ga_util.html" />
 
  <py:def function="page_title">Publisher Analytics for ${g.site_title}</py:def>
 
  <py:match path="primarysidebar">
  <li class="widget-container boxed widget_text">
  <h4>Publishers</h4>
  <p>
  Dataset views records the number of times a specific dataset page has been viewed. Visits records the number of unique site visits.
  </p>
  <p>
  Note: this data does not include API calls.
  </p>
  </li>
  <li class="widget-container boxed widget_text">
  <h4>Download</h4>
  <p><center>
  <a class="btn button" href="${h.url_for(controller='ckanext.ga_report.controller:GaPublisherReport',action='csv',month=c.month or 'all')}">Download as CSV</a></center>
  </p>
  </li>
  </py:match>
 
 
  <div py:match="content">
  <h1>Site Usage</h1>
 
  ${usage_nav('Publishers', None)}
 
 
  <form class="form-inline" action="${h.url_for(controller='ckanext.ga_report.controller:GaPublisherReport',action='index')}" method="get">
  <div class="controls">
  <select name="month">
  <option value='' py:attrs="{'selected': 'selected' if not c.month else None}">All time</option>
 
  <py:for each="val,desc in c.months">
  <option value='${val}' py:attrs="{'selected': 'selected' if c.month == val else None}">${desc}</option>
  </py:for>
  </select>
  <input class="btn button" type='submit' value="Update"/>
  </div>
  </form>
 
  <table class="table table-condensed table-bordered table-striped">
  <tr>
  <th>Publisher</th>
  <th>Dataset Views</th>
  <th>Visits</th>
  </tr>
  <py:for each="publisher, views, visits in c.top_publishers">
  <tr>
  <td>${h.link_to(publisher.title, h.url_for(controller='ckanext.ga_report.controller:GaPublisherReport', action='read', id=publisher.name))}
  </td>
  <td>${views}</td>
  <td>${visits}</td>
  </tr>
  </py:for>
  </table>
 
 
  </div>
 
  <xi:include href="../../layout.html" />
 
  <py:def function="optional_footer">
  <script type='text/javascript'>
  $('.nav-tabs li a').click(function (e) {
  e.preventDefault();
  $(this).tab('show');
  })
  </script>
  </py:def>
 
  </html>
 
 
  <html xmlns:py="http://genshi.edgewall.org/"
  xmlns:i18n="http://genshi.edgewall.org/i18n"
  xmlns:xi="http://www.w3.org/2001/XInclude"
  py:strip="">
  <py:if test="dataset_count == 0">
  <p>We do not currently have analytics data for ${publisher.title}</p>
  </py:if>
 
  <py:if test="dataset_count > 0">
  <div class="popular_datasets">
  <ul>
  <py:for each="dataset,views,visits in datasets">
  <li>
  ${h.link_to(dataset.title, h.url_for(controller='package', action='read', id=dataset.name))}
  </li>
  </py:for>
  </ul>
  <p class="">${h.link_to("More analytics for " + publisher.title, h.url_for(controller='ckanext.ga_report.controller:GaPublisherReport',action='read',id=publisher.name))}</p>
  </div>
  </py:if>
  </html>
 
 
 
 
  <html xmlns:py="http://genshi.edgewall.org/"
  xmlns:i18n="http://genshi.edgewall.org/i18n"
  xmlns:xi="http://www.w3.org/2001/XInclude"
  py:strip="">
 
  <xi:include href="../ga_util.html" />
 
  <py:def function="page_title">Analytics for ${g.site_title}</py:def>
 
  <py:match path="primarysidebar">
  <li class="widget-container boxed widget_text">
  <h4>Publishers</h4>
  <p>
  Dataset views records the number of times a specific dataset page has been viewed. Visits records the number of unique site visits.
  </p>
  <p>
  Note: this data does not include API calls.
  </p>
  </li>
 
  <li class="widget-container boxed widget_text">
  <h4>Download</h4>
  <p><center>
  <a class="btn button" href="${h.url_for(controller='ckanext.ga_report.controller:GaPublisherReport',action='publisher_csv',id=c.publisher.name,month=c.month or 'all')}">Download as CSV</a></center>
  </p>
  </li>
  </py:match>
 
 
  <div py:match="content">
  <h1>Site Usage</h1>
 
  ${usage_nav(c.publisher.title, c.publisher)}
 
  <form class="form-inline" action="${h.url_for(controller='ckanext.ga_report.controller:GaPublisherReport',action='read',id=c.publisher.name)}" method="get">
  <div class="controls">
  <select name="month">
  <option value='' py:attrs="{'selected': 'selected' if not c.month else None}">All time</option>
  <py:for each="val,desc in c.months">
  <option value='${val}' py:attrs="{'selected': 'selected' if c.month == val else None}">${desc}</option>
  </py:for>
  </select>
  <input class="btn button" type='submit' value="Update"/>
  </div>
  </form>
 
  <table class="table table-condensed table-bordered table-striped">
  <tr>
  <th>Dataset</th>
  <th>Views</th>
  <th>Visits</th>
  </tr>
  <py:for each="package, views, visits in c.top_packages">
  <tr>
  <td>${h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name))}
  </td>
  <td>${views}</td>
  <td>${visits}</td>
  </tr>
  </py:for>
  </table>
 
 
  </div>
 
  <xi:include href="../../layout.html" />
  </html>
 
 
 
 
  <html xmlns:py="http://genshi.edgewall.org/"
  xmlns:i18n="http://genshi.edgewall.org/i18n"
  xmlns:xi="http://www.w3.org/2001/XInclude"
  py:strip="">
 
  <xi:include href="../ga_util.html" />
 
  <py:def function="page_title">Site usage</py:def>
 
  <py:match path="primarysidebar">
  <li class="widget-container boxed widget_text">
  <h4>Site-wide</h4>
  <p>
  Note: this data does not include API calls and some values have been rounded up to 2 decimal places. Where there are a large number of browser versions they have been grouped together.
  </p>
  </li>
  <li class="widget-container boxed widget_text">
  <h4>Download</h4>
  <p><center>
  <a class="btn button" href="${h.url_for(controller='ckanext.ga_report.controller:GaReport',action='csv',month=c.month or 'all')}">Download as CSV</a></center>
  </p>
  </li>
 
  </py:match>
 
  <div py:match="content">
  <h1>Site Usage</h1>
 
  ${usage_nav('Site-wide', None)}
 
  <form class="form-inline" action="${h.url_for(controller='ckanext.ga_report.controller:GaReport',action='index')}" method="get">
  <div class="controls">
  <select name="month">
  <option value='' py:attrs="{'selected': 'selected' if not c.month else None}">All time</option>
 
  <py:for each="val,desc in c.months">
  <option value='${val}' py:attrs="{'selected': 'selected' if c.month == val else None}">${desc}</option>
  </py:for>
  </select>
  <input class="btn button" type='submit' value="Update"/>
  </div>
  </form>
 
  <div class="tabbable">
  <ul class="nav nav-tabs">
  <li class="active"><a href="#totals" data-toggle="tab">Totals</a></li>
  <li class="dropdown">
  <a href="#browsers" class="dropdown-toggle" data-toggle="dropdown">Browsers
  <b class="caret"></b></a>
  <ul class="dropdown-menu">
  <li><a href="#browsers_names" data-toggle="tab">Browsers</a></li>
  <li><a href="#browsers_versions" data-toggle="tab">Versions</a></li>
  </ul>
  </li>
  <li class="dropdown">
  <a href="#browsers" class="dropdown-toggle" data-toggle="dropdown">Operating<br/>Systems
  <b class="caret"></b></a>
  <ul class="dropdown-menu">
  <li><a href="#os" data-toggle="tab">Operating Systems</a></li>
  <li><a href="#os_versions" data-toggle="tab">Versions</a></li>
  </ul>
  </li>
  <li><a href="#social_networks" data-toggle="tab">Social Networks</a></li>
  <li><a href="#languages" data-toggle="tab">Languages</a></li>
  <li><a href="#country" data-toggle="tab">Country</a></li>
  </ul>
  <div class="tab-content">
  <div class="tab-pane active" id="totals">
  <table class="table table-condensed table-bordered table-striped">
  <tr>
  <th>Name</th>
  <th>Value</th>
  </tr>
  <py:for each="name, value in c.global_totals">
  <tr>
  <td>${name}</td>
  <td>${value}</td>
  </tr>
  </py:for>
  </table>
  </div>
  <div class="tab-pane" id="browsers_versions">
  ${stat_table(c.browser_versions)}
  </div>
  <div class="tab-pane" id="browsers_names">
  ${stat_table(c.browsers)}
  </div>
  <div class="tab-pane" id="os">
  ${stat_table(c.os)}
  </div>
  <div class="tab-pane" id="os_versions">
  ${stat_table(c.os_versions)}
  </div>
  <div class="tab-pane" id="social_networks">
  ${stat_table(c.social_networks)}
  </div>
  <div class="tab-pane" id="languages">
  ${stat_table(c.languages)}
  </div>
  <div class="tab-pane" id="country">
  ${stat_table(c.country)}
  </div>
 
 
  </div>
  </div>
 
 
 
  </div>
 
  <xi:include href="../../layout.html" />
 
  <py:def function="optional_footer">
  <script type='text/javascript'>
  $('.dropdown-toggle').dropdown();
  $('.nav-tabs li a').click(function (e) {
  e.preventDefault();
  $(this).tab('show');
  })
  </script>
  </py:def>
  </html>
 
 
 
 
import os import os
import datetime import datetime
from nose.tools import assert_equal from nose.tools import assert_equal
from ckanext.ga_report.download_analytics import DownloadAnalytics from ckanext.ga_report.download_analytics import DownloadAnalytics
from ckanext.ga_report.ga_auth import (init_service, get_profile_id) from ckanext.ga_report.ga_auth import (init_service, get_profile_id)
from ckanext.ga_report.ga_model import init_tables from ckanext.ga_report.ga_model import init_tables
   
class TestAPI: class TestAPI:
   
@classmethod @classmethod
def setup_class(cls): def setup_class(cls):
if not os.path.exists("token.dat") or not os.path.exists("credentials.json"): if not os.path.exists("token.dat") or not os.path.exists("credentials.json"):
print '*' * 60 print '*' * 60
print "Tests may not run without first having run the auth process" print "Tests may not run without first having run the auth process"
print '*' * 60 print '*' * 60
init_tables() init_tables()
   
@classmethod @classmethod
def teardown_class(cls): def teardown_class(cls):
pass pass
   
def test_latest(self): def test_latest(self):
svc = init_service("token.dat", "credentials.json") svc = init_service("token.dat", "credentials.json")
try: try:
downloader = DownloadAnalytics(svc, profile_id=get_profile_id(svc)) downloader = DownloadAnalytics(svc, profile_id=get_profile_id(svc))
downloader.latest() downloader.latest()
except Exception as e: except Exception as e:
assert False, e assert False, e
   
   
def test_since(self): def test_since(self):
svc = init_service("token.dat", "credentials.json") svc = init_service("token.dat", "credentials.json")
downloader = DownloadAnalytics(svc, profile_id=get_profile_id(svc)) downloader = DownloadAnalytics(svc, profile_id=get_profile_id(svc))
try: try:
downloader.since_date(datetime.datetime.now() - datetime.timedelta(days=-30)) downloader.for_date(datetime.datetime.now() - datetime.timedelta(days=-30))
except Exception as e: except Exception as e:
assert False, e assert False, e
   
file:a/setup.py -> file:b/setup.py
from setuptools import setup, find_packages from setuptools import setup, find_packages
import sys, os import sys, os
   
version = '0.1' version = '0.1'
   
setup( setup(
name='ckanext-ga-report', name='ckanext-ga-report',
version=version, version=version,
description="GA reporting for CKAN", description="GA reporting for CKAN",
long_description="""\ long_description="""\
""", """,
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
keywords='', keywords='',
author='David Read', author='David Read',
author_email='david.read@hackneyworkshop.com', author_email='david.read@hackneyworkshop.com',
url='', url='',
license='', license='',
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
namespace_packages=['ckanext', 'ckanext.ga_report'], namespace_packages=['ckanext', 'ckanext.ga_report'],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=[ install_requires=[
'gdata', 'gdata',
'google-api-python-client' 'google-api-python-client'
], ],
entry_points=\ entry_points=\
""" """
[ckan.plugins] [ckan.plugins]
# Add plugins here, eg # Add plugins here, eg
ga-report=ckanext.ga_report.plugin:GaReportPlugin ga-report=ckanext.ga_report.plugin:GAReportPlugin
   
[paste.paster_command] [paste.paster_command]
loadanalytics = ckanext.ga_report.command:LoadAnalytics loadanalytics = ckanext.ga_report.command:LoadAnalytics
initdb = ckanext.ga_report.command:InitDB initdb = ckanext.ga_report.command:InitDB
  getauthtoken = ckanext.ga_report.command:GetAuthToken
""", """,
) )