yandex_metrika.py 13 KB

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