Changed the icons in the minor nav to simpler icons
Changed the icons in the minor nav to simpler icons

file:a/.gitignore -> file:b/.gitignore
*.py[co] *.py[co]
*.py~ *.py~
.gitignore .gitignore
   
# Packages # Packages
*.egg *.egg
*.egg-info *.egg-info
dist dist
build build
eggs eggs
parts parts
bin bin
var var
sdist sdist
develop-eggs develop-eggs
.installed.cfg .installed.cfg
   
  # Private info
  credentials.json
  token.dat
   
# Installer logs # Installer logs
pip-log.txt pip-log.txt
   
# Unit test / coverage reports # Unit test / coverage reports
.coverage .coverage
.tox .tox
   
#Translations #Translations
*.mo *.mo
   
#Mr Developer #Mr Developer
.mr.developer.cfg .mr.developer.cfg
   
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.username = googleaccount@gmail.com googleanalytics.account = Account name (i.e. data.gov.uk, see top level item at https://www.google.com/analytics)
googleanalytics.password = googlepassword  
ga-report.period = monthly ga-report.period = monthly
   
Note that your password 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
  --------------
   
  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>`_
   
  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.
   
  4. Go to the `API Access pane <https://code.google.com/apis/console/#:access>`_
   
  5. Click Create an OAuth 2.0 client ID....
   
  6. Fill out the Branding Information fields and click Next.
   
  7. In Client ID Settings, set Application type to Installed application.
   
  8. Click Create client ID
   
  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
  following command, which will store your oauth token in a file called token.dat once you have finished
  giving permission in the browser::
   
  $ 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):: 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 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
   
  * **all** - data for all time (since 2010)
   
  * **latest** - (default) just the 'latest' data
   
  * **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
   
from ckan.lib.cli import CkanCommand from ckan.lib.cli import CkanCommand
# No other CKAN imports allowed until _load_config is run, or logging is disabled # No other CKAN imports allowed until _load_config is run,
  # 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):
  """ Get's the Google auth token
   
  Usage: paster getauthtoken <credentials_file>
   
  Where <credentials_file> is the file name containing the details
  for the service (obtained from https://code.google.com/apis/console).
  By default this is set to credentials.json
  """
  summary = __doc__.split('\n')[0]
  usage = __doc__
  max_args = 0
  min_args = 0
   
  def command(self):
  """
  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
  act as a form of verification instead of just getting the token and
  assuming it is correct.
  """
  from ga_auth import init_service
  init_service('token.dat',
  self.args[0] if self.args
  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 <time-period> Usage: paster loadanalytics <tokenfile> <time-period>
   
Where <time-period> is: Where <tokenfile> is the name of the auth token file from
  the getauthtoken step.
   
  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 = 1 max_args = 2
min_args = 0 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
downloader = DownloadAnalytics() from ga_auth import (init_service, get_profile_id)
   
time_period = self.args[0] if self.args else 'latest' try:
  svc = init_service(self.args[0], None)
  except TypeError:
  print ('Have you correctly run the getauthtoken task and '
  'specified the correct file here')
  return
   
  downloader = DownloadAnalytics(svc, profile_id=get_profile_id(svc))
   
  time_period = self.args[1] if self.args and len(self.args) > 1 \
  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, g, 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, GA_ReferralStat
   
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 months'
  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', '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 == 'New visits':
  val = "%s%%" % val
  if key in ['Bounces', 'Total page views', 'Total visits']:
  val = int(val)
   
  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 page views', 'Total visits']:
  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'
  }
   
  def shorten_name(name, length=60):
  return (name[:length] + '..') if len(name) > 60 else name
   
  def fill_out_url(url):
  import urlparse
  return urlparse.urljoin(g.site_url, url)
   
  c.social_referrer_totals, c.social_referrers = [], []
  q = model.Session.query(GA_ReferralStat)
  q = q.filter(GA_ReferralStat.period_name==c.month) if c.month else q
  q = q.order_by('ga_referrer.count::int desc')
  for entry in q.all():
  c.social_referrers.append((shorten_name(entry.url), fill_out_url(entry.url),
  entry.source,entry.count))
   
  q = model.Session.query(GA_ReferralStat.url,
  func.sum(GA_ReferralStat.count).label('count'))
  q = q.filter(GA_ReferralStat.period_name==c.month) if c.month else q
  q = q.order_by('count desc').group_by(GA_ReferralStat.url)
  for entry in q.all():
  c.social_referrer_totals.append((shorten_name(entry[0]), fill_out_url(entry[0]),'',
  entry[1]))
   
   
  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
  if k == 'Social sources':
  total = sum([x for n,x in c.global_totals if n == 'Total visits'])
  else:
  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 months'
  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 months'
  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 logging import logging
import datetime import datetime
  import collections
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):  
  def __init__(self, service=None, profile_id=None):
self.period = config['ga-report.period'] self.period = config['ga-report.period']
  self.service = service
def all_(self): self.profile_id = profile_id
pass  
   
  def specific_month(self, date):
  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, a)\ 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) data = self.download(start_date, end_date, '~/dataset/[a-z0-9-_]+')
log.info('Storing 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, )
   
@classmethod data = self.download(start_date, end_date, '~/publisher/[a-z0-9-_]+')
def download(cls, start_date, end_date): log.info('Storing Publisher Analytics for period "%s"',
  self.get_full_period_name(period_name, period_complete_day))
  self.store(period_name, period_complete_day, data,)
   
  ga_model.update_publisher_stats(period_name) # about 30 seconds.
  self.sitewide_stats( period_name )
   
  self.update_social_info(period_name, start_date, end_date)
   
  def update_social_info(self, period_name, start_date, end_date):
  start_date = start_date.strftime('%Y-%m-%d')
  end_date = end_date.strftime('%Y-%m-%d')
  query = 'ga:hasSocialSourceReferral=~Yes$'
  metrics = 'ga:entrances'
  sort = '-ga:entrances'
   
  # Supported query params at
  # https://developers.google.com/analytics/devguides/reporting/core/v3/reference
  results = self.service.data().ga().get(
  ids='ga:' + self.profile_id,
  filters=query,
  start_date=start_date,
  metrics=metrics,
  sort=sort,
  dimensions="ga:landingPagePath,ga:socialNetwork",
  max_results=10000,
  end_date=end_date).execute()
  data = collections.defaultdict(list)
  rows = results.get('rows',[])
  for row in rows:
  from ga_model import _normalize_url
  data[_normalize_url(row[0])].append( (row[1], int(row[2]),) )
  ga_model.update_social(period_name, data)
   
   
  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')
# url query = 'ga:pagePath=%s$' % path
#query = 'ga:pagePath=~^%s,ga:pagePath=~^%s' % \ metrics = 'ga:uniquePageviews, ga:visitors'
# (PACKAGE_URL, self.resource_url_tag)  
query = 'ga:pagePath=~^/dataset/'  
metrics = 'ga:uniquePageviews'  
sort = '-ga:uniquePageviews' sort = '-ga:uniquePageviews'
for entry in GA.ga_query(query_filter=query,  
from_date=start_date, # Supported query params at
  # https://developers.google.com/analytics/devguides/reporting/core/v3/reference
  results = self.service.data().ga().get(
  ids='ga:' + self.profile_id,
  filters=query,
  start_date=start_date,
metrics=metrics, metrics=metrics,
sort=sort, sort=sort,
to_date=end_date): dimensions="ga:pagePath",
print entry max_results=10000,
import pdb; pdb.set_trace() end_date=end_date).execute()
for dim in entry.dimension:  
if dim.name == "ga:pagePath": if os.getenv('DEBUG'):
package = dim.value import pprint
count = entry.get_metric( pprint.pprint(results)
'ga:uniquePageviews').value or 0 print 'Total results: %s' % results.get('totalResults')
packages[package] = int(count)  
return packages packages = []
  for entry in results.get('rows'):
  (loc,pageviews,visits) = entry
  packages.append( ('http:/' + loc, pageviews, visits,) ) # Temporary hack
  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):
  import calendar
  year, month = period_name.split('-')
  _, last_day_of_month = calendar.monthrange(int(year), int(month))
   
  start_date = '%s-01' % period_name
  end_date = '%s-%s' % (period_name, last_day_of_month)
  print 'Sitewide_stats for %s (%s -> %s)' % (period_name, start_date, end_date)
   
  funcs = ['_totals_stats', '_social_stats', '_os_stats',
  '_locale_stats', '_browser_stats', '_mobile_stats']
  for f in funcs:
  print ' + Fetching %s stats' % f.split('_')[1]
  getattr(self, f)(start_date, end_date, period_name)
   
  def _get_results(result_data, f):
  data = {}
  for result in result_data:
  key = f(result)
  data[key] = data.get(key,0) + result[1]
  return data
   
  def _totals_stats(self, start_date, end_date, period_name):
  """ 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]})
   
  results = self.service.data().ga().get(
  ids='ga:' + self.profile_id,
  start_date=start_date,
  metrics='ga:pageviewsPerVisit,ga:bounces,ga:avgTimeOnSite,ga:percentNewVisits,ga:visitors',
  max_results=10000,
  end_date=end_date).execute()
  result_data = results.get('rows')
  data = {
  'Pages per visit': result_data[0][0],
  'Bounces': result_data[0][1],
  'Average time on site': result_data[0][2],
  'New visits': result_data[0][3],
  'Total visits': result_data[0][4],
  }
  ga_model.update_sitewide_stats(period_name, "Totals", data)
   
   
  def _locale_stats(self, start_date, end_date, period_name):
  """ 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',
  dimensions="ga:language,ga:country",
  max_results=10000,
  end_date=end_date).execute()
  result_data = results.get('rows')
  data = {}
  for result in result_data:
  data[result[0]] = data.get(result[0], 0) + int(result[2])
  ga_model.update_sitewide_stats(period_name, "Languages", data)
   
  data = {}
  for result in result_data:
  data[result[1]] = data.get(result[1], 0) + int(result[2])
  ga_model.update_sitewide_stats(period_name, "Country", data)
   
   
  def _social_stats(self, start_date, end_date, period_name):
  """ 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',
  dimensions="ga:socialNetwork,ga:referralPath",
  max_results=10000,
  end_date=end_date).execute()
  result_data = results.get('rows')
  twitter_links = []
  data = {}
  for result in result_data:
  if not result[0] == '(not set)':
  data[result[0]] = data.get(result[0], 0) + int(result[2])
  if result[0] == 'Twitter':
  twitter_links.append(result[1])
  ga_model.update_sitewide_stats(period_name, "Social sources", data)
   
   
  def _os_stats(self, start_date, end_date, period_name):
  """ Operating system stats """
  results = self.service.data().ga().get(
  ids='ga:' + self.profile_id,
  start_date=start_date,
  metrics='ga:uniquePageviews',
  sort='-ga:uniquePageviews',
  dimensions="ga:operatingSystem,ga:operatingSystemVersion",
  max_results=10000,
  end_date=end_date).execute()
  result_data = results.get('rows')
  data = {}
  for result in result_data:
  data[result[0]] = data.get(result[0], 0) + int(result[2])
  ga_model.update_sitewide_stats(period_name, "Operating Systems", data)
   
  data = {}
  for result in result_data:
  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):
  """ 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',
  dimensions="ga:browser,ga:browserVersion",
  max_results=10000,
  end_date=end_date).execute()
  result_data = results.get('rows')
  data = {}
  for result in result_data:
  data[result[0]] = data.get(result[0], 0) + int(result[2])
  ga_model.update_sitewide_stats(period_name, "Browsers", data)
   
  data = {}
  for result in result_data:
  key = "%s (%s)" % (result[0], result[1])
  data[key] = result[2]
  ga_model.update_sitewide_stats(period_name, "Browser versions", data)
   
   
  def _mobile_stats(self, start_date, end_date, period_name):
  """ 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',
  dimensions="ga:mobileDeviceBranding, ga:mobileDeviceInfo",
  max_results=10000,
  end_date=end_date).execute()
   
  result_data = results.get('rows')
  data = {}
  for result in result_data:
  data[result[0]] = data.get(result[0], 0) + int(result[2])
  ga_model.update_sitewide_stats(period_name, "Mobile brands", data)
   
  data = {}
  for result in result_data:
  data[result[1]] = data.get(result[1], 0) + int(result[2])
  ga_model.update_sitewide_stats(period_name, "Mobile devices", data)
   
  import os
  import httplib2
  from apiclient.discovery import build
  from oauth2client.client import flow_from_clientsecrets
  from oauth2client.file import Storage
  from oauth2client.tools import run
 
  from pylons import config
 
 
  def _prepare_credentials(token_filename, credentials_filename):
  """
  Either returns the user's oauth credentials or uses the credentials
  file to generate a token (by forcing the user to login in the browser)
  """
  storage = Storage(token_filename)
  credentials = storage.get()
 
  if credentials is None or credentials.invalid:
  flow = flow_from_clientsecrets(credentials_filename,
  scope='https://www.googleapis.com/auth/analytics.readonly',
  message="Can't find the credentials file")
  credentials = run(flow, storage)
 
  return credentials
 
 
  def init_service(token_file, credentials_file):
  """
  Given a file containing the user's oauth token (and another with
  credentials in case we need to generate the token) will return a
  service object representing the analytics API.
  """
  http = httplib2.Http()
 
  credentials = _prepare_credentials(token_file, credentials_file)
  http = credentials.authorize(http) # authorize the http object
 
  return build('analytics', 'v3', http=http)
 
 
  def get_profile_id(service):
  """
  Get the profile ID for this user and the service specified by the
  'googleanalytics.id' configuration option. This function iterates
  over all of the accounts available to the user who invoked the
  service to find one where the account name matches (in case the
  user has several).
  """
  accounts = service.management().accounts().list().execute()
 
  if not accounts.get('items'):
  return None
 
  accountName = config.get('googleanalytics.account')
  webPropertyId = config.get('googleanalytics.id')
  for acc in accounts.get('items'):
  if acc.get('name') == accountName:
  accountId = acc.get('id')
 
  webproperties = service.management().webproperties().list(accountId=accountId).execute()
 
  profiles = service.management().profiles().list(
  accountId=accountId, webPropertyId=webPropertyId).execute()
 
  if profiles.get('items'):
  return profiles.get('items')[0].get('id')
 
  return None
 
import re import re
import uuid import uuid
   
from sqlalchemy import Table, Column, MetaData from sqlalchemy import Table, Column, MetaData
from sqlalchemy import types from sqlalchemy import types
from sqlalchemy.sql import select, text from sqlalchemy.sql import select
  from sqlalchemy.orm import mapper
from sqlalchemy import func from sqlalchemy import func
   
import ckan.model as model import ckan.model as model
from ckan.model.types import JsonType  
from ckan.lib.base import * from ckan.lib.base import *
   
def make_uuid(): def make_uuid():
return unicode(uuid.uuid4()) return unicode(uuid.uuid4())
   
  metadata = MetaData()
   
   
   
  class GA_Url(object):
   
  def __init__(self, **kwargs):
  for k,v in kwargs.items():
  setattr(self, k, v)
   
  url_table = Table('ga_url', metadata,
  Column('id', types.UnicodeText, primary_key=True,
  default=make_uuid),
  Column('period_name', types.UnicodeText),
  Column('period_complete_day', types.Integer),
  Column('pageviews', types.UnicodeText),
  Column('visitors', types.UnicodeText),
  Column('url', types.UnicodeText),
  Column('department_id', types.UnicodeText),
  )
  mapper(GA_Url, url_table)
   
   
  class GA_Stat(object):
   
  def __init__(self, **kwargs):
  for k,v in kwargs.items():
  setattr(self, k, v)
   
  stat_table = Table('ga_stat', metadata,
  Column('id', types.UnicodeText, primary_key=True,
  default=make_uuid),
  Column('period_name', types.UnicodeText),
  Column('stat_name', types.UnicodeText),
  Column('key', types.UnicodeText),
  Column('value', types.UnicodeText), )
  mapper(GA_Stat, stat_table)
   
   
  class GA_Publisher(object):
   
  def __init__(self, **kwargs):
  for k,v in kwargs.items():
  setattr(self, k, v)
   
  pub_table = Table('ga_publisher', metadata,
  Column('id', types.UnicodeText, primary_key=True,
  default=make_uuid),
  Column('period_name', types.UnicodeText),
  Column('publisher_name', types.UnicodeText),
  Column('views', types.UnicodeText),
  Column('visitors', types.UnicodeText),
  Column('toplevel', types.Boolean, default=False),
  Column('subpublishercount', types.Integer, default=0),
  Column('parent', types.UnicodeText),
  )
  mapper(GA_Publisher, pub_table)
   
   
  class GA_ReferralStat(object):
   
  def __init__(self, **kwargs):
  for k,v in kwargs.items():
  setattr(self, k, v)
   
  referrer_table = Table('ga_referrer', metadata,
  Column('id', types.UnicodeText, primary_key=True,
  default=make_uuid),
  Column('period_name', types.UnicodeText),
  Column('source', types.UnicodeText),
  Column('url', types.UnicodeText),
  Column('count', types.Integer),
  )
  mapper(GA_ReferralStat, referrer_table)
   
   
   
def init_tables(): def init_tables():
metadata = MetaData()  
package_stats = Table('ga_url', metadata,  
Column('id', types.UnicodeText, primary_key=True, default=make_uuid),  
Column('period_name', types.UnicodeText),  
Column('period_complete_day', types.Integer),  
Column('visits', types.Integer),  
Column('group_id', types.String(60)),  
Column('next_page', JsonType),  
)  
metadata.create_all(model.meta.engine) metadata.create_all(model.meta.engine)
   
   
cached_tables = {} cached_tables = {}
   
   
def get_table(name): def get_table(name):
if name not in cached_tables: if name not in cached_tables:
meta = MetaData() meta = MetaData()
meta.reflect(bind=model.meta.engine) meta.reflect(bind=model.meta.engine)
table = meta.tables[name] table = meta.tables[name]
cached_tables[name] = table cached_tables[name] = table
return cached_tables[name] return cached_tables[name]
   
   
def _normalize_url(url): def _normalize_url(url):
'''Strip off the hostname etc. Do this before storing it. '''Strip off the hostname etc. Do this before storing it.
   
>>> normalize_url('http://data.gov.uk/dataset/weekly_fuel_prices') >>> normalize_url('http://data.gov.uk/dataset/weekly_fuel_prices')
'/dataset/weekly_fuel_prices' '/dataset/weekly_fuel_prices'
''' '''
url = re.sub('https?://(www\.)?data.gov.uk', '', url) # Deliberately leaving a /
return url url = url.replace('http:/','')
  return '/' + '/'.join(url.split('/')[2:])
   
   
def _get_department_id_of_url(url): def _get_department_id_of_url(url):
# e.g. /dataset/fuel_prices # e.g. /dataset/fuel_prices
# e.g. /dataset/fuel_prices/resource/e63380d4 # e.g. /dataset/fuel_prices/resource/e63380d4
dataset_match = re.match('/dataset/([^/]+)(/.*)?', url) dataset_match = re.match('/dataset/([^/]+)(/.*)?', url)
if dataset_match: if dataset_match:
dataset_ref = dataset_match.groups()[0] dataset_ref = dataset_match.groups()[0]
dataset = model.Package.get(dataset_ref) dataset = model.Package.get(dataset_ref)
if dataset: if dataset:
publisher_groups = dataset.get_groups('publisher') publisher_groups = dataset.get_groups('publisher')
if publisher_groups: if publisher_groups:
return publisher_groups[0].id return publisher_groups[0].name
  else:
  publisher_match = re.match('/publisher/([^/]+)(/.*)?', url)
  if publisher_match:
  return publisher_match.groups()[0]
   
   
  def update_sitewide_stats(period_name, stat_name, data):
  for k,v in data.iteritems():
  item = model.Session.query(GA_Stat).\
  filter(GA_Stat.period_name==period_name).\
  filter(GA_Stat.key==k).\
  filter(GA_Stat.stat_name==stat_name).first()
  if item:
  item.period_name = period_name
  item.key = k
  item.value = v
  model.Session.add(item)
  else:
  # create the row
  values = {'id': make_uuid(),
  'period_name': period_name,
  'key': k,
  'value': v,
  'stat_name': stat_name
  }
  model.Session.add(GA_Stat(**values))
  model.Session.commit()
   
   
   
def update_url_stats(period_name, period_complete_day, url_data): def update_url_stats(period_name, period_complete_day, url_data):
table = get_table('ga_url') for url, views, visitors in url_data:
connection = model.Session.connection()  
for url, views, next_page in url_data:  
url = _normalize_url(url) url = _normalize_url(url)
department_id = _get_department_id_of_url(url) department_id = _get_department_id_of_url(url)
   
# see if the row for this url & month is in the table already # see if the row for this url & month is in the table already
s = select([func.count(id_col)], item = model.Session.query(GA_Url).\
table.c.period_name == period_name, filter(GA_Url.period_name==period_name).\
table.c.url == url) filter(GA_Url.url==url).first()
count = connection.execute(s).fetchone() if item:
if count and count[0]: item.period_name = period_name
# update the row item.pageviews = views
connection.execute(table.update()\ item.visitors = visitors
.where(table.c.period_name == period_name, item.department_id = department_id
table.c.url == url)\ model.Session.add(item)
.values(period_complete_day=period_complete_day,  
views=views,  
department_id=department_id,  
next_page=next_page))  
else: else:
# create the row # create the row
values = {'period_name': period_name, values = {'id': make_uuid(),
  'period_name': period_name,
'period_complete_day': period_complete_day, 'period_complete_day': period_complete_day,
'url': url, 'url': url,
'views': views, 'pageviews': views,
'department_id': department_id, 'visitors': visitors,
'next_page': next_page} 'department_id': department_id
connection.execute(stats.insert()\ }
.values(**values)) model.Session.add(GA_Url(**values))
  model.Session.commit()
   
   
  def update_social(period_name, data):
  # Clean up first.
  model.Session.query(GA_ReferralStat).\
  filter(GA_ReferralStat.period_name==period_name).delete()
   
  for url,data in data.iteritems():
  for entry in data:
  source = entry[0]
  count = entry[1]
   
  item = model.Session.query(GA_ReferralStat).\
  filter(GA_ReferralStat.period_name==period_name).\
  filter(GA_ReferralStat.source==source).\
  filter(GA_ReferralStat.url==url).first()
  if item:
  item.count = item.count + count
  model.Session.add(item)
  else:
  # create the row
  values = {'id': make_uuid(),
  'period_name': period_name,
  'source': source,
  'url': url,
  'count': count,
  }
  model.Session.add(GA_ReferralStat(**values))
  model.Session.commit()
   
  def update_publisher_stats(period_name):
  """
  Updates the publisher stats from the data retrieved for /dataset/*
  and /publisher/*. Will run against each dataset and generates the
  totals for the entire tree beneath each publisher.
  """
  toplevel = get_top_level()
  publishers = model.Session.query(model.Group).\
  filter(model.Group.type=='publisher').\
  filter(model.Group.state=='active').all()
  for publisher in publishers:
  views, visitors, subpub = update_publisher(period_name, publisher, publisher.name)
  parent, parents = '', publisher.get_groups('publisher')
  if parents:
  parent = parents[0].name
  item = model.Session.query(GA_Publisher).\
  filter(GA_Publisher.period_name==period_name).\
  filter(GA_Publisher.publisher_name==publisher.name).first()
  if item:
  item.views = views
  item.visitors = visitors
  item.publisher_name = publisher.name
  item.toplevel = publisher in toplevel
  item.subpublishercount = subpub
  item.parent = parent
  model.Session.add(item)
  else:
  # create the row
  values = {'id': make_uuid(),
  'period_name': period_name,
  'publisher_name': publisher.name,
  'views': views,
  'visitors': visitors,
  'toplevel': publisher in toplevel,
  'subpublishercount': subpub,
  'parent': parent
  }
  model.Session.add(GA_Publisher(**values))
  model.Session.commit()
   
   
  def update_publisher(period_name, pub, part=''):
  views,visitors,subpub = 0, 0, 0
  for publisher in go_down_tree(pub):
  subpub = subpub + 1
  items = model.Session.query(GA_Url).\
  filter(GA_Url.period_name==period_name).\
  filter(GA_Url.department_id==publisher.name).all()
  for item in items:
  views = views + int(item.pageviews)
  visitors = visitors + int(item.visitors)
   
  return views, visitors, (subpub-1)
   
   
  def get_top_level():
  '''Returns the top level publishers.'''
  return model.Session.query(model.Group).\
  outerjoin(model.Member, model.Member.table_id == model.Group.id and \
  model.Member.table_name == 'group' and \
  model.Member.state == 'active').\
  filter(model.Member.id==None).\
  filter(model.Group.type=='publisher').\
  order_by(model.Group.name).all()
   
  def get_children(publisher):
  '''Finds child publishers for the given publisher (object). (Not recursive)'''
  from ckan.model.group import HIERARCHY_CTE
  return model.Session.query(model.Group).\
  from_statement(HIERARCHY_CTE).params(id=publisher.id, type='publisher').\
  all()
   
  def go_down_tree(publisher):
  '''Provided with a publisher object, it walks down the hierarchy and yields each publisher,
  including the one you supply.'''
  yield publisher
  for child in get_children(publisher):
  for grandchild in go_down_tree(child):
  yield grandchild
   
  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="social_table(items, with_source=False)" class="table table-condensed table-bordered table-striped">
  <tr>
  <th>Name</th>
  <th py:if="with_source">Source</th>
  <th>Visits</th>
  </tr>
  <py:for each="name, url, source, count in items">
  <tr>
  <td><a href="${url}">${name}</a></td>
  <td py:if="with_source">${source}</td>
  <td>${count}</td>
  </tr>
  </py:for>
  </table>
 
 
  <table py:def="stat_table(items, title='Views')" class="table table-condensed table-bordered table-striped">
  <tr>
  <th>Name</th>
  <th>% ${title}</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.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.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.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 months</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 for <a href="${h.url_for(controller='ckanext.dgu.controllers.publisher:PublisherController',action='read',id=c.publisher.name)}">${c.publisher.title}</a>. 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 months</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 months</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 class="dropdown">
  <a href="#browsers" class="dropdown-toggle" data-toggle="dropdown">Social Networks
  <b class="caret"></b></a>
  <ul class="dropdown-menu">
  <li><a href="#social_networks" data-toggle="tab">All networks</a></li>
  <li><a href="#social_referrals_totals" data-toggle="tab">Referral links</a></li>
  </ul>
  </li>
 
  <li><a href="#social_networks" data-toggle="tab"></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_referrals_totals">
  <p>Number of visits to urls referred from social networks</p>
  ${social_table(c.social_referrer_totals)}
  </div>
  <div class="tab-pane" id="social_networks">
  <p>Percentage of visits referred from these social networks</p>
 
  ${stat_table(c.social_networks, 'Visits')}
  </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');
  })
  alert(window.location.hash);
  </script>
  </py:def>
  </html>
 
 
 
 
  import os
  import datetime
  from nose.tools import assert_equal
  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_model import init_tables
 
  class TestAPI:
 
  @classmethod
  def setup_class(cls):
  if not os.path.exists("token.dat") or not os.path.exists("credentials.json"):
  print '*' * 60
  print "Tests may not run without first having run the auth process"
  print '*' * 60
  init_tables()
 
  @classmethod
  def teardown_class(cls):
  pass
 
  def test_latest(self):
  svc = init_service("token.dat", "credentials.json")
  try:
  downloader = DownloadAnalytics(svc, profile_id=get_profile_id(svc))
  downloader.latest()
  except Exception as e:
  assert False, e
 
 
  def test_since(self):
  svc = init_service("token.dat", "credentials.json")
  downloader = DownloadAnalytics(svc, profile_id=get_profile_id(svc))
  try:
  downloader.for_date(datetime.datetime.now() - datetime.timedelta(days=-30))
  except Exception as e:
  assert False, e
 
  import os
  from nose.tools import assert_equal
  from ckanext.ga_report.ga_auth import (init_service, get_profile_id)
 
  class TestAuth:
 
  @classmethod
  def setup_class(cls):
  if not os.path.exists("token.dat") or not os.path.exists("credentials.json"):
  print '*' * 60
  print "Tests may not run without first having run the auth process"
  print '*' * 60
 
  @classmethod
  def teardown_class(cls):
  pass
 
  def test_init(self):
  try:
  res = init_service(None, None)
  assert False, "Init service worked without credentials or tokens"
  except TypeError:
  pass
 
  def test_init_with_token(self):
  res = init_service("token.dat", None)
  assert res is not None, "Init service worked without credentials"
 
  def test_init_with_token_and_credentials(self):
  res = init_service("token.dat", "credentials.json")
  assert res is not None, "Unable to create service with valid details"
 
  def test_init_with_redentials(self):
  #res = init_service("", "credentials.json")
  # Triggers the auth flow via the browser
  pass
 
  def test_get_profile(self):
  svc = init_service("token.dat", "credentials.json")
  profile = get_profile_id(svc)
  assert profile is not None, "Unable to find a profile given configured UA id and user details"
  {
  "installed": {
  "client_id": "",
  "client_secret": "",
  "redirect_uris": [""],
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://accounts.google.com/o/oauth2/token"
  }
  }
 
 
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'
], ],
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
""", """,
) )