google_analytics.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. import datetime
  2. import os
  3. from django import forms
  4. from django.core.urlresolvers import reverse
  5. from django.forms import Widget
  6. from django.forms.utils import flatatt
  7. from django.utils import formats
  8. from django.utils.html import format_html
  9. from django.utils.safestring import mark_safe
  10. from googleapiclient.discovery import build
  11. import httplib2
  12. from jet.modules import DashboardModule
  13. from oauth2client.client import flow_from_clientsecrets, OAuth2Credentials, AccessTokenRefreshError
  14. from django.utils.translation import ugettext_lazy as _
  15. from django.conf import settings
  16. try:
  17. from django.utils.encoding import force_unicode
  18. except ImportError:
  19. from django.utils.encoding import force_text as force_unicode
  20. JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE = getattr(
  21. settings,
  22. 'JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE',
  23. ''
  24. )
  25. class GoogleAnalyticsClient:
  26. credential = None
  27. analytics_service = None
  28. def __init__(self, credential=None, redirect_uri=None):
  29. self.FLOW = flow_from_clientsecrets(
  30. JET_MODULE_GOOGLE_ANALYTICS_CLIENT_SECRETS_FILE,
  31. scope='https://www.googleapis.com/auth/analytics.readonly',
  32. redirect_uri=redirect_uri
  33. )
  34. if credential is not None:
  35. self.set_credential(OAuth2Credentials.from_json(credential))
  36. def get_oauth_authorize_url(self, state=''):
  37. self.FLOW.params['state'] = state
  38. authorize_url = self.FLOW.step1_get_authorize_url()
  39. return authorize_url
  40. def set_credential(self, credential):
  41. self.credential = credential
  42. self.set_analytics_service(self.credential)
  43. def set_credential_from_request(self, request):
  44. self.set_credential(self.FLOW.step2_exchange(request.GET))
  45. def set_analytics_service(self, credential):
  46. http = httplib2.Http()
  47. http = credential.authorize(http)
  48. self.analytics_service = build('analytics', 'v3', http=http)
  49. def api_profiles(self):
  50. if self.analytics_service is None:
  51. return None, None
  52. try:
  53. profiles = self.analytics_service.management().profiles().list(
  54. accountId='~all',
  55. webPropertyId='~all'
  56. ).execute()
  57. return profiles['items'], None
  58. except (TypeError, KeyError) as e:
  59. return None, e
  60. def api_ga(self, profile_id, date1, date2, group=None):
  61. if self.analytics_service is None:
  62. return None, None
  63. if group == 'day':
  64. dimensions = 'ga:date'
  65. elif group == 'week':
  66. dimensions = 'ga:year,ga:week'
  67. elif group == 'month':
  68. dimensions = 'ga:year,ga:month'
  69. else:
  70. dimensions = ''
  71. try:
  72. data = self.analytics_service.data().ga().get(
  73. ids='ga:' + profile_id,
  74. start_date=date1.strftime('%Y-%m-%d'),
  75. end_date=date2.strftime('%Y-%m-%d'),
  76. metrics='ga:users,ga:sessions,ga:pageviews',
  77. dimensions=dimensions
  78. ).execute()
  79. return data, None
  80. except TypeError as e:
  81. return None, e
  82. class CredentialWidget(Widget):
  83. module = None
  84. def render(self, name, value, attrs=None):
  85. if value and len(value) > 0:
  86. link = '<a href="%s">Revoke access</a>' % reverse('jet:google-analytics-revoke', kwargs={'pk': self.module.model.pk})
  87. else:
  88. link = '<a href="%s">Grant access</a>' % reverse('jet:google-analytics-grant', kwargs={'pk': self.module.model.pk})
  89. attrs = self.build_attrs({
  90. 'type': 'hidden',
  91. 'name': 'credential',
  92. })
  93. attrs['value'] = force_unicode(value)
  94. return format_html('%s<input{} />' % link, flatatt(attrs))
  95. class GoogleAnalyticsSettingsForm(forms.Form):
  96. credential = forms.CharField(label=_('Credential'), widget=CredentialWidget)
  97. counter = forms.ChoiceField(label=_('Counter'))
  98. period = forms.ChoiceField(label=_('Statistics period'), choices=(
  99. (0, _('Today')),
  100. (6, _('Last week')),
  101. (30, _('Last month')),
  102. (31 * 3 - 1, _('Last quarter')),
  103. (364, _('Last year')),
  104. ))
  105. def set_module(self, module):
  106. self.fields['credential'].widget.module = module
  107. self.set_counter_choices(module)
  108. def set_counter_choices(self, module):
  109. counters = module.counters()
  110. if counters is not None:
  111. self.fields['counter'].choices = (('', _('-- none --')),)
  112. self.fields['counter'].choices.extend(map(lambda x: (x['id'], x['websiteUrl']), counters))
  113. else:
  114. label = _('grant access first') if module.credential is None else _('counters loading failed')
  115. self.fields['counter'].choices = (('', '-- %s -- ' % label),)
  116. class GoogleAnalyticsChartSettingsForm(GoogleAnalyticsSettingsForm):
  117. show = forms.ChoiceField(label=_('Show'), choices=(
  118. ('ga:users', _('Users')),
  119. ('ga:sessions', _('Sessions')),
  120. ('ga:pageviews', _('Views')),
  121. ))
  122. group = forms.ChoiceField(label=_('Group'), choices=(
  123. ('day', _('By day')),
  124. ('week', _('By week')),
  125. ('month', _('By month')),
  126. ))
  127. class GoogleAnalyticsPeriodVisitorsSettingsForm(GoogleAnalyticsSettingsForm):
  128. group = forms.ChoiceField(label=_('Group'), choices=(
  129. ('day', _('By day')),
  130. ('week', _('By week')),
  131. ('month', _('By month')),
  132. ))
  133. class GoogleAnalyticsBase(DashboardModule):
  134. settings_form = GoogleAnalyticsSettingsForm
  135. ajax_load = True
  136. contrast = True
  137. period = None
  138. credential = None
  139. counter = None
  140. error = None
  141. class Media:
  142. js = ('jet/vendor/chart.js/Chart.min.js', 'jet/modules/google_analytics.js')
  143. def __init__(self, title=None, period=None, **kwargs):
  144. kwargs.update({'period': period})
  145. super(GoogleAnalyticsBase, self).__init__(title, **kwargs)
  146. def settings_dict(self):
  147. return {
  148. 'period': self.period,
  149. 'credential': self.credential,
  150. 'counter': self.counter
  151. }
  152. def load_settings(self, settings):
  153. try:
  154. self.period = int(settings.get('period'))
  155. except TypeError:
  156. self.period = 0
  157. self.credential = settings.get('credential')
  158. self.counter = settings.get('counter')
  159. def init_with_context(self, context):
  160. raise NotImplementedError('subclasses of GoogleAnalytics must provide a init_with_context() method')
  161. def counters(self):
  162. try:
  163. client = GoogleAnalyticsClient(self.credential)
  164. profiles, exception = client.api_profiles()
  165. return profiles
  166. except Exception:
  167. return None
  168. def get_grouped_date(self, data, group):
  169. if group == 'week':
  170. date = datetime.datetime.strptime(
  171. '%s-%s-%s' % (data['ga_year'], data['ga_week'], '0'),
  172. '%Y-%W-%w'
  173. )
  174. elif group == 'month':
  175. date = datetime.datetime.strptime(data['ga_year'] + data['ga_month'], '%Y%m')
  176. else:
  177. date = datetime.datetime.strptime(data['ga_date'], '%Y%m%d')
  178. return date
  179. def format_grouped_date(self, data, group):
  180. date = self.get_grouped_date(data, group)
  181. if group == 'week':
  182. date = u'%s — %s' % (
  183. (date - datetime.timedelta(days=6)).strftime('%d.%m'),
  184. date.strftime('%d.%m')
  185. )
  186. elif group == 'month':
  187. date = date.strftime('%b, %Y')
  188. else:
  189. date = formats.date_format(date, 'DATE_FORMAT')
  190. return date
  191. def counter_attached(self):
  192. if self.credential is None:
  193. 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}))
  194. return False
  195. elif self.counter is None:
  196. 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}))
  197. return False
  198. else:
  199. return True
  200. def api_ga(self, group=None):
  201. if self.counter_attached():
  202. date1 = datetime.datetime.utcnow() - datetime.timedelta(days=self.period)
  203. date2 = datetime.datetime.utcnow()
  204. try:
  205. client = GoogleAnalyticsClient(self.credential)
  206. result, exception = client.api_ga(self.counter, date1, date2, group)
  207. if exception is not None:
  208. raise exception
  209. return result
  210. except Exception as e:
  211. error = _('API request failed.')
  212. if isinstance(e, AccessTokenRefreshError):
  213. error += _(' Try to <a href="%s">revoke and grant access</a> again') % reverse('jet:update_module', kwargs={'pk': self.model.pk})
  214. self.error = mark_safe(error)
  215. class GoogleAnalyticsVisitorsTotals(GoogleAnalyticsBase):
  216. title = _('Google Analytics visitors totals')
  217. template = 'jet/dashboard/modules/google_analytics_visitors_totals.html'
  218. def init_with_context(self, context):
  219. result = self.api_ga()
  220. if result is not None:
  221. try:
  222. self.children.append({'title': _('users'), 'value': result['totalsForAllResults']['ga:users']})
  223. self.children.append({'title': _('sessions'), 'value': result['totalsForAllResults']['ga:sessions']})
  224. self.children.append({'title': _('views'), 'value': result['totalsForAllResults']['ga:pageviews']})
  225. except KeyError:
  226. self.error = _('Bad server response')
  227. class GoogleAnalyticsVisitorsChart(GoogleAnalyticsBase):
  228. title = _('Google Analytics visitors chart')
  229. template = 'jet/dashboard/modules/google_analytics_visitors_chart.html'
  230. style = 'overflow-x: auto;'
  231. show = None
  232. group = None
  233. settings_form = GoogleAnalyticsChartSettingsForm
  234. def settings_dict(self):
  235. settings = super(GoogleAnalyticsVisitorsChart, self).settings_dict()
  236. settings['show'] = self.show
  237. settings['group'] = self.group
  238. return settings
  239. def load_settings(self, settings):
  240. super(GoogleAnalyticsVisitorsChart, self).load_settings(settings)
  241. self.show = settings.get('show')
  242. self.group = settings.get('group')
  243. def init_with_context(self, context):
  244. result = self.api_ga(self.group)
  245. if result is not None:
  246. try:
  247. for data in result['rows']:
  248. row_data = {}
  249. i = 0
  250. for column in result['columnHeaders']:
  251. row_data[column['name'].replace(':', '_')] = data[i]
  252. i += 1
  253. date = self.get_grouped_date(row_data, self.group)
  254. self.children.append((date, row_data[self.show.replace(':', '_')]))
  255. except KeyError:
  256. self.error = _('Bad server response')
  257. class GoogleAnalyticsPeriodVisitors(GoogleAnalyticsBase):
  258. title = _('Google Analytics period visitors')
  259. template = 'jet/dashboard/modules/google_analytics_period_visitors.html'
  260. group = None
  261. contrast = False
  262. settings_form = GoogleAnalyticsPeriodVisitorsSettingsForm
  263. def settings_dict(self):
  264. settings = super(GoogleAnalyticsPeriodVisitors, self).settings_dict()
  265. settings['group'] = self.group
  266. return settings
  267. def load_settings(self, settings):
  268. super(GoogleAnalyticsPeriodVisitors, self).load_settings(settings)
  269. self.group = settings.get('group')
  270. def init_with_context(self, context):
  271. result = self.api_ga(self.group)
  272. if result is not None:
  273. try:
  274. for data in reversed(result['rows']):
  275. row_data = {}
  276. i = 0
  277. for column in result['columnHeaders']:
  278. row_data[column['name'].replace(':', '_')] = data[i]
  279. i += 1
  280. date = self.format_grouped_date(row_data, self.group)
  281. self.children.append((date, row_data))
  282. except KeyError:
  283. self.error = _('Bad server response')