yandex_metrika.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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 jet.modules import DashboardModule
  11. from django.utils.translation import ugettext_lazy as _
  12. from django.conf import settings
  13. try:
  14. from urllib import request
  15. from urllib.parse import urlencode
  16. from urllib.error import URLError, HTTPError
  17. except ImportError:
  18. import urllib2 as request
  19. from urllib2 import URLError, HTTPError
  20. from urllib import urlencode
  21. JET_MODULE_YANDEX_METRIKA_CLIENT_ID = getattr(settings, 'JET_MODULE_YANDEX_METRIKA_CLIENT_ID', '')
  22. JET_MODULE_YANDEX_METRIKA_CLIENT_SECRET = getattr(settings, 'JET_MODULE_YANDEX_METRIKA_CLIENT_SECRET', '')
  23. class YandexMetrikaClient:
  24. OAUTH_BASE_URL = 'https://oauth.yandex.ru/'
  25. API_BASE_URL = 'https://api-metrika.yandex.ru/'
  26. CLIENT_ID = JET_MODULE_YANDEX_METRIKA_CLIENT_ID
  27. CLIENT_SECRET = JET_MODULE_YANDEX_METRIKA_CLIENT_SECRET
  28. def __init__(self, access_token=None):
  29. self.access_token = access_token
  30. def request(self, base_url, url, data=None, headers=None):
  31. url = '%s%s' % (base_url, url)
  32. if data is not None:
  33. data = urlencode(data).encode()
  34. if headers is None:
  35. headers = {}
  36. req = request.Request(url, data, headers)
  37. try:
  38. f = request.urlopen(req)
  39. result = f.read().decode()
  40. result = json.loads(result)
  41. except URLError as e:
  42. return None, e
  43. return result, None
  44. def get_oauth_authorize_url(self, state=''):
  45. return '%sauthorize' \
  46. '?response_type=code' \
  47. '&state=%s' \
  48. '&client_id=%s' % (self.OAUTH_BASE_URL, state, self.CLIENT_ID)
  49. def oauth_request(self, url, data=None):
  50. return self.request(self.OAUTH_BASE_URL, url, data)
  51. def oath_token_request(self, code):
  52. data = {
  53. 'grant_type': 'authorization_code',
  54. 'code': code,
  55. 'client_id': self.CLIENT_ID,
  56. 'client_secret': self.CLIENT_SECRET
  57. }
  58. return self.oauth_request('token', data)
  59. def api_request(self, url, data=None):
  60. headers = None
  61. if self.access_token is not None:
  62. headers = {'Authorization': 'OAuth %s' % self.access_token}
  63. return self.request(self.API_BASE_URL, url, data, headers)
  64. def api_counters_request(self):
  65. return self.api_request('counters.json')
  66. def api_stat_traffic_summary(self, counter, date1, date2, group=None):
  67. if group is None:
  68. group = 'day'
  69. return self.api_request('stat/traffic/summary.json?id=%s&date1=%s&date2=%s&group=%s' % (
  70. counter,
  71. date1.strftime('%Y%m%d'),
  72. date2.strftime('%Y%m%d'),
  73. group
  74. ))
  75. class AccessTokenWidget(Widget):
  76. module = None
  77. def render(self, name, value, attrs=None):
  78. if value and len(value) > 0:
  79. link = '<a href="%s">Revoke access</a>' % reverse('jet:yandex-metrika-revoke', kwargs={'pk': self.module.model.pk})
  80. else:
  81. link = '<a href="%s">Grant access</a>' % reverse('jet:yandex-metrika-grant', kwargs={'pk': self.module.model.pk})
  82. return format_html('%s<input type="hidden" name="access_token" value="%s">' % (link, value))
  83. class YandexMetrikaSettingsForm(forms.Form):
  84. access_token = forms.CharField(label=_('Token'), widget=AccessTokenWidget)
  85. counter = forms.ChoiceField(label=_('Counter'))
  86. period = forms.ChoiceField(label=_('Statistics period'), choices=(
  87. (0, _('Today')),
  88. (6, _('Last week')),
  89. (30, _('Last month')),
  90. (31 * 3 - 1, _('Last quarter')),
  91. (364, _('Last year')),
  92. ))
  93. def set_module(self, module):
  94. self.fields['access_token'].widget.module = module
  95. self.set_counter_choices(module)
  96. def set_counter_choices(self, module):
  97. counters = module.counters()
  98. if counters is not None:
  99. self.fields['counter'].choices = (('', _('-- none --')),)
  100. self.fields['counter'].choices.extend(map(lambda x: (x['id'], x['site']), counters))
  101. else:
  102. label = _('grant access first') if module.access_token is None else _('counters loading failed')
  103. self.fields['counter'].choices = (('', '-- %s -- ' % label),)
  104. class YandexMetrikaChartSettingsForm(YandexMetrikaSettingsForm):
  105. show = forms.ChoiceField(label=_('Show'), choices=(
  106. ('visitors', _('Visitors')),
  107. ('visits', _('Visits')),
  108. ('page_views', _('Views')),
  109. ))
  110. group = forms.ChoiceField(label=_('Group'), choices=(
  111. ('day', _('By day')),
  112. ('week', _('By week')),
  113. ('month', _('By month')),
  114. ))
  115. class YandexMetrikaPeriodVisitorsSettingsForm(YandexMetrikaSettingsForm):
  116. group = forms.ChoiceField(label=_('Group'), choices=(
  117. ('day', _('By day')),
  118. ('week', _('By week')),
  119. ('month', _('By month')),
  120. ))
  121. class YandexMetrikaBase(DashboardModule):
  122. settings_form = YandexMetrikaSettingsForm
  123. ajax_load = True
  124. contrast = True
  125. period = None
  126. access_token = None
  127. expires_in = None
  128. token_type = None
  129. counter = None
  130. error = None
  131. def settings_dict(self):
  132. return {
  133. 'period': self.period,
  134. 'access_token': self.access_token,
  135. 'expires_in': self.expires_in,
  136. 'token_type': self.token_type,
  137. 'counter': self.counter
  138. }
  139. def load_settings(self, settings):
  140. try:
  141. self.period = int(settings.get('period'))
  142. except TypeError:
  143. self.period = 0
  144. self.access_token = settings.get('access_token')
  145. self.expires_in = settings.get('expires_in')
  146. self.token_type = settings.get('token_type')
  147. self.counter = settings.get('counter')
  148. def init_with_context(self, context):
  149. raise NotImplementedError('subclasses of YandexMetrika must provide a init_with_context() method')
  150. def counters(self):
  151. client = YandexMetrikaClient(self.access_token)
  152. counters, exception = client.api_counters_request()
  153. if counters is not None:
  154. return counters['counters']
  155. else:
  156. return None
  157. def format_grouped_date(self, date, group):
  158. if group == 'week':
  159. date = u'%s — %s' % (
  160. (date - datetime.timedelta(days=7)).strftime('%d.%m'),
  161. date.strftime('%d.%m')
  162. )
  163. elif group == 'month':
  164. date = date.strftime('%b, %Y')
  165. else:
  166. date = formats.date_format(date, 'DATE_FORMAT')
  167. return date
  168. def counter_attached(self):
  169. if self.access_token is None:
  170. self.error = mark_safe(_('Please <a href="%s">attach Yandex account and choose Yandex Metrika counter</a> to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk}))
  171. return False
  172. elif self.counter is None:
  173. self.error = mark_safe(_('Please <a href="%s">select Yandex Metrika counter</a> to start using widget') % reverse('jet:update_module', kwargs={'pk': self.model.pk}))
  174. return False
  175. else:
  176. return True
  177. def api_stat_traffic_summary(self, group=None):
  178. if self.counter_attached():
  179. date1 = datetime.datetime.utcnow() - datetime.timedelta(days=self.period)
  180. date2 = datetime.datetime.utcnow()
  181. client = YandexMetrikaClient(self.access_token)
  182. result, exception = client.api_stat_traffic_summary(self.counter, date1, date2, group)
  183. if exception is not None:
  184. error = _('API request failed.')
  185. if isinstance(exception, HTTPError) and exception.code == 403:
  186. error += _(' Try to <a href="%s">revoke and grant access</a> again') % reverse('jet:update_module', kwargs={'pk': self.model.pk})
  187. self.error = mark_safe(error)
  188. else:
  189. return result
  190. class YandexMetrikaVisitorsTotals(YandexMetrikaBase):
  191. title = _('Yandex Metrika visitors totals')
  192. template = 'jet/dashboard/modules/yandex_metrika_visitors_totals.html'
  193. def __init__(self, title=None, period=None, **kwargs):
  194. kwargs.update({'period': period})
  195. super(YandexMetrikaVisitorsTotals, self).__init__(title, **kwargs)
  196. def init_with_context(self, context):
  197. result = self.api_stat_traffic_summary()
  198. if result is not None:
  199. try:
  200. self.children.append({'title': _('visitors'), 'value': result['totals']['visitors']})
  201. self.children.append({'title': _('visits'), 'value': result['totals']['visits']})
  202. self.children.append({'title': _('views'), 'value': result['totals']['page_views']})
  203. except KeyError:
  204. self.error = _('Bad server response')
  205. class YandexMetrikaVisitorsChart(YandexMetrikaBase):
  206. title = _('Yandex Metrika visitors chart')
  207. template = 'jet/dashboard/modules/yandex_metrika_visitors_chart.html'
  208. style = 'overflow-x: auto;'
  209. show = None
  210. group = None
  211. settings_form = YandexMetrikaChartSettingsForm
  212. class Media:
  213. js = ('jet/vendor/chart.js/Chart.min.js', 'jet/dashboard_modules/yandex_metrika.js')
  214. def __init__(self, title=None, period=None, show=None, group=None, **kwargs):
  215. kwargs.update({'period': period, 'show': show, 'group': group})
  216. super(YandexMetrikaVisitorsChart, self).__init__(title, **kwargs)
  217. def settings_dict(self):
  218. settings = super(YandexMetrikaVisitorsChart, self).settings_dict()
  219. settings['show'] = self.show
  220. settings['group'] = self.group
  221. return settings
  222. def load_settings(self, settings):
  223. super(YandexMetrikaVisitorsChart, self).load_settings(settings)
  224. self.show = settings.get('show')
  225. self.group = settings.get('group')
  226. def init_with_context(self, context):
  227. result = self.api_stat_traffic_summary(self.group)
  228. if result is not None:
  229. try:
  230. for data in result['data']:
  231. date = datetime.datetime.strptime(data['date'], '%Y%m%d')
  232. key = self.show if self.show is not None else 'visitors'
  233. self.children.append((date, data[key]))
  234. except KeyError:
  235. self.error = _('Bad server response')
  236. class YandexMetrikaPeriodVisitors(YandexMetrikaBase):
  237. title = _('Yandex Metrika period visitors')
  238. template = 'jet/dashboard/modules/yandex_metrika_period_visitors.html'
  239. group = None
  240. contrast = False
  241. settings_form = YandexMetrikaPeriodVisitorsSettingsForm
  242. def __init__(self, title=None, period=None, group=None, **kwargs):
  243. kwargs.update({'period': period, 'group': group})
  244. super(YandexMetrikaPeriodVisitors, self).__init__(title, **kwargs)
  245. def settings_dict(self):
  246. settings = super(YandexMetrikaPeriodVisitors, self).settings_dict()
  247. settings['group'] = self.group
  248. return settings
  249. def load_settings(self, settings):
  250. super(YandexMetrikaPeriodVisitors, self).load_settings(settings)
  251. self.group = settings.get('group')
  252. def init_with_context(self, context):
  253. result = self.api_stat_traffic_summary(self.group)
  254. if result is not None:
  255. try:
  256. for data in reversed(result['data']):
  257. date = datetime.datetime.strptime(data['date'], '%Y%m%d')
  258. date = self.format_grouped_date(date, self.group)
  259. self.children.append((date, data))
  260. except KeyError:
  261. self.error = _('Bad server response')