google_analytics.py 12 KB

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