|
@@ -0,0 +1,348 @@
|
|
|
+import datetime
|
|
|
+import os
|
|
|
+from django import forms
|
|
|
+from django.core.urlresolvers import reverse
|
|
|
+from django.forms import Widget
|
|
|
+from django.forms.utils import flatatt
|
|
|
+from django.utils import formats
|
|
|
+from django.utils.html import format_html
|
|
|
+from django.utils.safestring import mark_safe
|
|
|
+from googleapiclient.discovery import build
|
|
|
+import httplib2
|
|
|
+from jet.modules import DashboardModule
|
|
|
+from oauth2client.client import flow_from_clientsecrets, OAuth2Credentials, AccessTokenRefreshError
|
|
|
+from django.utils.translation import ugettext_lazy as _
|
|
|
+from django.conf import settings
|
|
|
+try:
|
|
|
+ from django.utils.encoding import force_unicode
|
|
|
+except ImportError:
|
|
|
+ from django.utils.encoding import force_text as force_unicode
|
|
|
+
|
|
|
+JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE = getattr(
|
|
|
+ settings,
|
|
|
+ 'JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE',
|
|
|
+ ''
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+class GoogleAnalyticsClient:
|
|
|
+ credential = None
|
|
|
+ analytics_service = None
|
|
|
+
|
|
|
+ def __init__(self, credential=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 credential is not None:
|
|
|
+ self.set_credential(OAuth2Credentials.from_json(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 = '<a href="%s">Revoke access</a>' % reverse('jet:google-analytics-revoke', kwargs={'pk': self.module.model.pk})
|
|
|
+ else:
|
|
|
+ link = '<a href="%s">Grant access</a>' % reverse('jet:google-analytics-grant', kwargs={'pk': self.module.model.pk})
|
|
|
+
|
|
|
+ attrs = self.build_attrs({
|
|
|
+ 'type': 'hidden',
|
|
|
+ 'name': 'credential',
|
|
|
+ })
|
|
|
+ attrs['value'] = force_unicode(value)
|
|
|
+
|
|
|
+ return format_html('%s<input{} />' % link, flatatt(attrs))
|
|
|
+
|
|
|
+
|
|
|
+class GoogleAnalyticsSettingsForm(forms.Form):
|
|
|
+ credential = forms.CharField(label=_('Credential'), 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 = (('', _('-- none --')),)
|
|
|
+ self.fields['counter'].choices.extend(map(lambda x: (x['id'], x['websiteUrl']), counters))
|
|
|
+ else:
|
|
|
+ label = _('grant access first') if module.credential is None else _('counters loading failed')
|
|
|
+ self.fields['counter'].choices = (('', '-- %s -- ' % label),)
|
|
|
+
|
|
|
+
|
|
|
+class GoogleAnalyticsChartSettingsForm(GoogleAnalyticsSettingsForm):
|
|
|
+ show = forms.ChoiceField(label=_('Show'), choices=(
|
|
|
+ ('ga:users', _('Users')),
|
|
|
+ ('ga:sessions', _('Sessions')),
|
|
|
+ ('ga:pageviews', _('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
|
|
|
+
|
|
|
+ class Media:
|
|
|
+ js = ('jet/vendor/chart.js/Chart.min.js', 'jet/modules/google_analytics.js')
|
|
|
+
|
|
|
+ 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.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.credential)
|
|
|
+ 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 <a href="%s">attach Google account and choose Google Analytics counter</a> to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk}))
|
|
|
+ return False
|
|
|
+ elif self.counter is None:
|
|
|
+ self.error = mark_safe(_('Please <a href="%s">select Google Analytics counter</a> to start using widget') % reverse('jet: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.utcnow() - datetime.timedelta(days=self.period)
|
|
|
+ date2 = datetime.datetime.utcnow()
|
|
|
+
|
|
|
+ try:
|
|
|
+ client = GoogleAnalyticsClient(self.credential)
|
|
|
+ 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 <a href="%s">revoke and grant access</a> again') % reverse('jet:update_module', kwargs={'pk': self.model.pk})
|
|
|
+ self.error = mark_safe(error)
|
|
|
+
|
|
|
+
|
|
|
+class GoogleAnalyticsVisitorsTotals(GoogleAnalyticsBase):
|
|
|
+ title = _('Google Analytics visitors totals')
|
|
|
+ template = 'jet/dashboard/modules/google_analytics_visitors_totals.html'
|
|
|
+
|
|
|
+ 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):
|
|
|
+ title = _('Google Analytics visitors chart')
|
|
|
+ template = 'jet/dashboard/modules/google_analytics_visitors_chart.html'
|
|
|
+ style = 'overflow-x: auto;'
|
|
|
+ show = None
|
|
|
+ group = None
|
|
|
+ settings_form = GoogleAnalyticsChartSettingsForm
|
|
|
+
|
|
|
+ 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):
|
|
|
+ title = _('Google Analytics period visitors')
|
|
|
+ template = 'jet/dashboard/modules/google_analytics_period_visitors.html'
|
|
|
+ group = None
|
|
|
+ contrast = False
|
|
|
+ settings_form = GoogleAnalyticsPeriodVisitorsSettingsForm
|
|
|
+
|
|
|
+ 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')
|