google_analytics.py 14 KB

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