# encoding: utf-8
import datetime
import json
from django import forms
try:
from django.core.urlresolvers import reverse
except ImportError: # Django 1.11
from django.urls import reverse
from django.forms import Widget
from django.utils import formats
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from googleapiclient.discovery import build
import httplib2
from jet.dashboard.modules import DashboardModule
from oauth2client.client import flow_from_clientsecrets, OAuth2Credentials, AccessTokenRefreshError, Storage
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django.utils.encoding import force_text
try:
from django.utils.encoding import force_unicode
except ImportError:
from django.utils.encoding import force_text as force_unicode
try:
from django.forms.utils import flatatt
except ImportError:
from django.forms.util import flatatt
JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE = getattr(
settings,
'JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE',
''
)
class ModuleCredentialStorage(Storage):
def __init__(self, module):
self.module = module
def locked_get(self):
pass
def locked_put(self, credentials):
pass
def locked_delete(self):
pass
def get(self):
try:
settings = json.loads(self.module.settings)
credential = settings['credential']
return OAuth2Credentials.from_json(credential)
except (ValueError, KeyError):
return None
def put(self, credentials):
self.module.update_settings({'credential': credentials.to_json()})
def delete(self):
self.module.pop_settings(('credential',))
class GoogleAnalyticsClient:
credential = None
analytics_service = None
def __init__(self, storage=None, redirect_uri=None):
self.FLOW = flow_from_clientsecrets(
JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE,
scope='https://www.googleapis.com/auth/analytics.readonly',
redirect_uri=redirect_uri
)
if storage is not None:
credential = storage.get()
credential.set_store(storage)
self.set_credential(credential)
def get_oauth_authorize_url(self, state=''):
self.FLOW.params['state'] = state
authorize_url = self.FLOW.step1_get_authorize_url()
return authorize_url
def set_credential(self, credential):
self.credential = credential
self.set_analytics_service(self.credential)
def set_credential_from_request(self, request):
self.set_credential(self.FLOW.step2_exchange(request.GET))
def set_analytics_service(self, credential):
http = httplib2.Http()
http = credential.authorize(http)
self.analytics_service = build('analytics', 'v3', http=http)
def api_profiles(self):
if self.analytics_service is None:
return None, None
try:
profiles = self.analytics_service.management().profiles().list(
accountId='~all',
webPropertyId='~all'
).execute()
return profiles['items'], None
except (TypeError, KeyError) as e:
return None, e
def api_ga(self, profile_id, date1, date2, group=None):
if self.analytics_service is None:
return None, None
if group == 'day':
dimensions = 'ga:date'
elif group == 'week':
dimensions = 'ga:year,ga:week'
elif group == 'month':
dimensions = 'ga:year,ga:month'
else:
dimensions = ''
try:
data = self.analytics_service.data().ga().get(
ids='ga:' + profile_id,
start_date=date1.strftime('%Y-%m-%d'),
end_date=date2.strftime('%Y-%m-%d'),
metrics='ga:users,ga:sessions,ga:pageviews',
dimensions=dimensions
).execute()
return data, None
except TypeError as e:
return None, e
class CredentialWidget(Widget):
module = None
def render(self, name, value, attrs=None):
if value and len(value) > 0:
link = '%s' % (
reverse('jet-dashboard:google-analytics-revoke', kwargs={'pk': self.module.model.pk}),
force_text(_('Revoke access'))
)
else:
link = '%s' % (
reverse('jet-dashboard:google-analytics-grant', kwargs={'pk': self.module.model.pk}),
force_text(_('Grant access'))
)
attrs = self.build_attrs({
'type': 'hidden',
'name': 'credential',
})
attrs['value'] = force_unicode(value) if value else ''
return format_html('%s' % link, flatatt(attrs))
class GoogleAnalyticsSettingsForm(forms.Form):
credential = forms.CharField(label=_('Access'), widget=CredentialWidget)
counter = forms.ChoiceField(label=_('Counter'))
period = forms.ChoiceField(label=_('Statistics period'), choices=(
(0, _('Today')),
(6, _('Last week')),
(30, _('Last month')),
(31 * 3 - 1, _('Last quarter')),
(364, _('Last year')),
))
def set_module(self, module):
self.fields['credential'].widget.module = module
self.set_counter_choices(module)
def set_counter_choices(self, module):
counters = module.counters()
if counters is not None:
self.fields['counter'].choices = (('', '-- %s --' % force_text(_('none'))),)
self.fields['counter'].choices.extend(map(lambda x: (x['id'], x['websiteUrl']), counters))
else:
label = force_text(_('grant access first')) if module.credential is None else force_text(_('counters loading failed'))
self.fields['counter'].choices = (('', '-- %s -- ' % label),)
class GoogleAnalyticsChartSettingsForm(GoogleAnalyticsSettingsForm):
show = forms.ChoiceField(label=_('Show'), choices=(
('ga:users', capfirst(_('users'))),
('ga:sessions', capfirst(_('sessions'))),
('ga:pageviews', capfirst(_('views'))),
))
group = forms.ChoiceField(label=_('Group'), choices=(
('day', _('By day')),
('week', _('By week')),
('month', _('By month')),
))
class GoogleAnalyticsPeriodVisitorsSettingsForm(GoogleAnalyticsSettingsForm):
group = forms.ChoiceField(label=_('Group'), choices=(
('day', _('By day')),
('week', _('By week')),
('month', _('By month')),
))
class GoogleAnalyticsBase(DashboardModule):
settings_form = GoogleAnalyticsSettingsForm
ajax_load = True
contrast = True
period = None
credential = None
counter = None
error = None
storage = None
def __init__(self, title=None, period=None, **kwargs):
kwargs.update({'period': period})
super(GoogleAnalyticsBase, self).__init__(title, **kwargs)
def settings_dict(self):
return {
'period': self.period,
'credential': self.credential,
'counter': self.counter
}
def load_settings(self, settings):
try:
self.period = int(settings.get('period'))
except TypeError:
self.period = 0
self.credential = settings.get('credential')
self.storage = ModuleCredentialStorage(self.model)
self.counter = settings.get('counter')
def init_with_context(self, context):
raise NotImplementedError('subclasses of GoogleAnalytics must provide a init_with_context() method')
def counters(self):
try:
client = GoogleAnalyticsClient(self.storage)
profiles, exception = client.api_profiles()
return profiles
except Exception:
return None
def get_grouped_date(self, data, group):
if group == 'week':
date = datetime.datetime.strptime(
'%s-%s-%s' % (data['ga_year'], data['ga_week'], '0'),
'%Y-%W-%w'
)
elif group == 'month':
date = datetime.datetime.strptime(data['ga_year'] + data['ga_month'], '%Y%m')
else:
date = datetime.datetime.strptime(data['ga_date'], '%Y%m%d')
return date
def format_grouped_date(self, data, group):
date = self.get_grouped_date(data, group)
if group == 'week':
date = u'%s — %s' % (
(date - datetime.timedelta(days=6)).strftime('%d.%m'),
date.strftime('%d.%m')
)
elif group == 'month':
date = date.strftime('%b, %Y')
else:
date = formats.date_format(date, 'DATE_FORMAT')
return date
def counter_attached(self):
if self.credential is None:
self.error = mark_safe(_('Please attach Google account and choose Google Analytics counter to start using widget') % reverse('jet-dashboard:update_module', kwargs={'pk': self.model.pk}))
return False
elif self.counter is None:
self.error = mark_safe(_('Please select Google Analytics counter to start using widget') % reverse('jet-dashboard:update_module', kwargs={'pk': self.model.pk}))
return False
else:
return True
def api_ga(self, group=None):
if self.counter_attached():
date1 = datetime.datetime.now() - datetime.timedelta(days=self.period)
date2 = datetime.datetime.now()
try:
client = GoogleAnalyticsClient(self.storage)
result, exception = client.api_ga(self.counter, date1, date2, group)
if exception is not None:
raise exception
return result
except Exception as e:
error = _('API request failed.')
if isinstance(e, AccessTokenRefreshError):
error += _(' Try to revoke and grant access again') % reverse('jet-dashboard:update_module', kwargs={'pk': self.model.pk})
self.error = mark_safe(error)
class GoogleAnalyticsVisitorsTotals(GoogleAnalyticsBase):
"""
Google Analytics widget that shows total number of users, sessions and viewers for a particular period of time.
Period may be following: Today, Last week, Last month, Last quarter, Last year
"""
title = _('Google Analytics visitors totals')
template = 'jet.dashboard/modules/google_analytics_visitors_totals.html'
#: Which period should be displayed. Allowed values - integer of days
period = None
def __init__(self, title=None, period=None, **kwargs):
kwargs.update({'period': period})
super(GoogleAnalyticsVisitorsTotals, self).__init__(title, **kwargs)
def init_with_context(self, context):
result = self.api_ga()
if result is not None:
try:
self.children.append({'title': _('users'), 'value': result['totalsForAllResults']['ga:users']})
self.children.append({'title': _('sessions'), 'value': result['totalsForAllResults']['ga:sessions']})
self.children.append({'title': _('views'), 'value': result['totalsForAllResults']['ga:pageviews']})
except KeyError:
self.error = _('Bad server response')
class GoogleAnalyticsVisitorsChart(GoogleAnalyticsBase):
"""
Google Analytics widget that shows users/sessions/viewer chart for a particular period of time.
Data is grouped by day, week or month
Period may be following: Today, Last week, Last month, Last quarter, Last year
"""
title = _('Google Analytics visitors chart')
template = 'jet.dashboard/modules/google_analytics_visitors_chart.html'
style = 'overflow-x: auto;'
#: Which period should be displayed. Allowed values - integer of days
period = None
#: What data should be shown. Possible values: ``ga:users``, ``ga:sessions``, ``ga:pageviews``
show = None
#: Sets grouping of data. Possible values: ``day``, ``week``, ``month``
group = None
settings_form = GoogleAnalyticsChartSettingsForm
class Media:
js = ('jet.dashboard/vendor/chart.js/Chart.min.js', 'jet.dashboard/dashboard_modules/google_analytics.js')
def __init__(self, title=None, period=None, show=None, group=None, **kwargs):
kwargs.update({'period': period, 'show': show, 'group': group})
super(GoogleAnalyticsVisitorsChart, self).__init__(title, **kwargs)
def settings_dict(self):
settings = super(GoogleAnalyticsVisitorsChart, self).settings_dict()
settings['show'] = self.show
settings['group'] = self.group
return settings
def load_settings(self, settings):
super(GoogleAnalyticsVisitorsChart, self).load_settings(settings)
self.show = settings.get('show')
self.group = settings.get('group')
def init_with_context(self, context):
result = self.api_ga(self.group)
if result is not None:
try:
for data in result['rows']:
row_data = {}
i = 0
for column in result['columnHeaders']:
row_data[column['name'].replace(':', '_')] = data[i]
i += 1
date = self.get_grouped_date(row_data, self.group)
self.children.append((date, row_data[self.show.replace(':', '_')]))
except KeyError:
self.error = _('Bad server response')
class GoogleAnalyticsPeriodVisitors(GoogleAnalyticsBase):
"""
Google Analytics widget that shows users, sessions and viewers for a particular period of time.
Data is grouped by day, week or month
Period may be following: Today, Last week, Last month, Last quarter, Last year
"""
title = _('Google Analytics period visitors')
template = 'jet.dashboard/modules/google_analytics_period_visitors.html'
#: Which period should be displayed. Allowed values - integer of days
period = None
#: Sets grouping of data. Possible values: ``day``, ``week``, ``month``
group = None
contrast = False
settings_form = GoogleAnalyticsPeriodVisitorsSettingsForm
def __init__(self, title=None, period=None, group=None, **kwargs):
kwargs.update({'period': period, 'group': group})
super(GoogleAnalyticsPeriodVisitors, self).__init__(title, **kwargs)
def settings_dict(self):
settings = super(GoogleAnalyticsPeriodVisitors, self).settings_dict()
settings['group'] = self.group
return settings
def load_settings(self, settings):
super(GoogleAnalyticsPeriodVisitors, self).load_settings(settings)
self.group = settings.get('group')
def init_with_context(self, context):
result = self.api_ga(self.group)
if result is not None:
try:
for data in reversed(result['rows']):
row_data = {}
i = 0
for column in result['columnHeaders']:
row_data[column['name'].replace(':', '_')] = data[i]
i += 1
date = self.format_grouped_date(row_data, self.group)
self.children.append((date, row_data))
except KeyError:
self.error = _('Bad server response')