google_analytics.py 12 KB

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