google_analytics.py 15 KB

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