google_analytics.py 13 KB

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