yandex_metrika.py 13 KB

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