yandex_metrika.py 11 KB

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