google_analytics.py 15 KB

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